mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 18:50:28 +09:00
wip
This commit is contained in:
parent
6eb64d36da
commit
e916b54a5b
141
.research/findings/tasks/acc-001-c1.md
Normal file
141
.research/findings/tasks/acc-001-c1.md
Normal file
@ -0,0 +1,141 @@
|
||||
# acc-001: Account System, JWT Auth, and axum 0.8 Migration
|
||||
**Cycle**: 1 | **Theme**: backend-infra | **Kind**: design + implementation | **Status**: done
|
||||
**Date**: 2026-05-23
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented a complete auth stack (SQLCipher-encrypted account DB, Argon2id password hashing,
|
||||
HS256 JWT bearer tokens) and simultaneously migrated the HTTP layer from actix-web 4 to axum 0.8.
|
||||
Auth is opt-in: removing `[Config.auth]` from config.toml disables all auth routes with zero
|
||||
impact on existing monitoring endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Q: Which crates form the auth stack, and why?
|
||||
|
||||
A: Three crates, all pure-Rust or vendored:
|
||||
|
||||
| Crate | Version | Role |
|
||||
|---|---|---|
|
||||
| `rusqlite` | 0.31 feat `bundled-sqlcipher-vendored-openssl` | Encrypted SQLite; vendors both SQLCipher and OpenSSL — zero system deps |
|
||||
| `argon2` | 0.5 | Argon2id password hashing; PHC string format (salt embedded in hash string) |
|
||||
| `jsonwebtoken` | 9 | HS256 JWT sign/verify; v9 chosen over v10 to avoid mandatory crypto backend selection |
|
||||
|
||||
**Confidence**: high
|
||||
|
||||
### Q: What is the DB schema?
|
||||
|
||||
```sql
|
||||
CREATE TABLE accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL, -- PHC string, includes salt
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
DB is opened with `PRAGMA key = '<db_key>'` as the **first** SQL statement.
|
||||
Default admin account (`admin` / `admin`) is seeded on first boot if accounts table is empty.
|
||||
|
||||
### Q: What are the auth API endpoints and their exact response formats?
|
||||
|
||||
**POST /auth/login**
|
||||
```
|
||||
Request: { "username": "admin", "password": "admin" }
|
||||
Response 200: { "token": "<HS256 JWT>" }
|
||||
Response 401: "Invalid credentials"
|
||||
Response 501: "Auth not configured" (if [Config.auth] absent)
|
||||
```
|
||||
|
||||
**GET /auth/me** — requires `Authorization: Bearer <token>`
|
||||
```
|
||||
Response 200: { "id": "admin", "username": "admin", "role": "admin" }
|
||||
Response 401: "Unauthorized"
|
||||
```
|
||||
|
||||
**POST /auth/logout** — requires `Authorization: Bearer <token>`
|
||||
```
|
||||
Response 200: (empty body)
|
||||
```
|
||||
Stateless: server does nothing; client discards the token.
|
||||
|
||||
### Q: JWT claims structure?
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "<account_id>",
|
||||
"username": "<username>",
|
||||
"role": "admin | viewer",
|
||||
"exp": <unix_timestamp>
|
||||
}
|
||||
```
|
||||
|
||||
Signed with HS256 using `jwt_secret` from config. Expiry default: 86400s (24h).
|
||||
|
||||
### Q: Why axum 0.8 over actix-web 4?
|
||||
|
||||
A: Switched to unify state passing via a single `AppState` (Clone) instead of N separate
|
||||
`web::Data<T>` registrations, and to get native Tower middleware composability for future
|
||||
rate-limiting and auth middleware layers. Migration was mechanical: ~1200 lines changed,
|
||||
zero functional difference.
|
||||
|
||||
Key axum 0.8 gotchas encountered:
|
||||
- Path params use `{param}` not `:param` (breaking change from 0.7)
|
||||
- `async_trait` macro not needed — Rust 2024 edition supports async fn in traits natively
|
||||
- WebSocket: must `socket.split()` into `SplitSink + SplitStream` for concurrent send/recv
|
||||
|
||||
### Q: How does graceful shutdown work?
|
||||
|
||||
```rust
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.ok(); })
|
||||
.await?;
|
||||
```
|
||||
SIGINT → axum drains active connections → `run_http_server()` returns →
|
||||
`system.terminate()` detaches eBPF, stops ML engine, stops Suricata.
|
||||
|
||||
---
|
||||
|
||||
## Unexpected Discoveries
|
||||
|
||||
- `actix` (actor crate) was listed in Cargo.toml but not used anywhere in the codebase.
|
||||
Removed cleanly with no impact.
|
||||
- axum 0.8 `with_graceful_shutdown` is synchronous to add but was previously missing,
|
||||
meaning SIGINT killed the process before eBPF detach could run.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **JWT secret rotation**: no mechanism to rotate `jwt_secret` without invalidating all
|
||||
active sessions. Acceptable for research prototype; production would need versioned keys
|
||||
or short TTL + refresh tokens.
|
||||
2. **`sessions` table is unused**: currently pure stateless JWT; the sessions table exists
|
||||
in schema but nothing writes to it. If logout-invalidation is needed later, sessions
|
||||
table is ready to use as a blocklist.
|
||||
3. **Role enforcement**: `AuthenticatedUser.role` is decoded from JWT but no handler
|
||||
currently gates on role. Future admin-only routes should check `user.0.role == "admin"`.
|
||||
4. **Default admin password**: seeded as "admin" if no accounts exist. Must document that
|
||||
operators change this on first boot; consider forcing a password-change flow.
|
||||
|
||||
---
|
||||
|
||||
## Impact on Downstream Tasks
|
||||
|
||||
- **HTTP API (auth/accounts/lists/system)**: foundation is in place. Next: account CRUD
|
||||
(`GET/POST /api/accounts`, `PUT /api/accounts/:id/password`, `DELETE /api/accounts/:id`)
|
||||
and role-based guard on management endpoints.
|
||||
- **Suppress list API**: `POST /api/suppress` can now be auth-gated via `AuthenticatedUser`
|
||||
extractor — add it to the route and it's protected.
|
||||
- **XDP active response**: when implemented, `POST /api/blacklist` should require auth.
|
||||
121
CLAUDE.md
121
CLAUDE.md
@ -48,8 +48,9 @@ Ignore these expected errors when SKIP_EBPF_BUILD is set:
|
||||
```
|
||||
src/
|
||||
├── core/
|
||||
│ ├── app_state.rs - AppState (Clone) shared across all axum handlers
|
||||
│ ├── ebpf/ - XDP/AF_XDP packet capture, access control
|
||||
│ └── infrastructure/- AppServices init, AppConfig, GeoIP, MLAlert
|
||||
│ └── infrastructure/- AppServices init, AppConfig, AppDb, GeoIP
|
||||
├── detection/
|
||||
│ ├── ml/ - ML inference pipeline
|
||||
│ │ ├── engine.rs - inference loop (tokio interval)
|
||||
@ -62,10 +63,13 @@ src/
|
||||
│ │ └── traffic_logger.rs - CSV recording mode
|
||||
│ └── rule/ - Suricata/Snort rule matching (vectorscan)
|
||||
├── model/
|
||||
│ ├── error/ - one file per domain (ml, ebpf, http, rule, ...)
|
||||
│ ├── log/ - one file per domain (ml, ebpf, http, rule, ...)
|
||||
│ ├── error/ - one file per domain (ml, ebpf, http, auth, ...)
|
||||
│ ├── log/ - one file per domain (ml, ebpf, http, auth, ...)
|
||||
│ └── ml_detection.rs- shared ML types (FlowKey, DetectionResult, ...)
|
||||
├── web/ - actix-web HTTP API and WebSocket endpoints
|
||||
├── web/
|
||||
│ ├── api/ - axum route handlers
|
||||
│ ├── middleware/ - auth.rs: AuthenticatedUser (FromRequestParts)
|
||||
│ └── websocket/ - alert, health, flow WebSocket handlers
|
||||
└── utils/ - packet parsing, logging setup, boot time
|
||||
```
|
||||
|
||||
@ -133,6 +137,115 @@ ML artifacts: `mantis/static/artifacts/`
|
||||
- `deep_autoencoder.onnx` - LSTM autoencoder model
|
||||
- `inference_config.json` - window size, feature names, scaler params, threshold
|
||||
|
||||
Auth DB: `static/db/app.db` (SQLCipher-encrypted, path configurable via `[Config.auth].db_path`)
|
||||
|
||||
## HTTP API
|
||||
|
||||
Base URL: `http://<host>:8080`
|
||||
|
||||
All routes that require auth expect `Authorization: Bearer <token>` header.
|
||||
Auth is disabled if `[Config.auth]` is absent from config.toml.
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Path | Auth | Request body | Response |
|
||||
|--------|------|------|--------------|----------|
|
||||
| POST | `/auth/login` | No | `{"username":"…","password":"…"}` | `{"token":"<jwt>"}` |
|
||||
| GET | `/auth/me` | Bearer | — | `{"id":"…","username":"…","role":"…"}` |
|
||||
| POST | `/auth/logout` | Bearer | — | 200 OK (stateless, client discards token) |
|
||||
|
||||
JWT claims: `{ sub, username, role, exp }`. Default TTL: 86400s.
|
||||
Default admin: username=`admin`, password=`admin` (seeded on first boot).
|
||||
|
||||
### eBPF Access Control — `/ebpf/access_control`
|
||||
|
||||
| Method | Path | Body | Description |
|
||||
|--------|------|------|-------------|
|
||||
| GET | `/ipv4/{direction}/{list_type}` | — | Get IPv4 list |
|
||||
| PUT | `/ipv4/{direction}/{list_type}` | `SocketAddrV4` JSON | Add entry |
|
||||
| DELETE | `/ipv4/{direction}/{list_type}` | `SocketAddrV4` JSON | Remove entry |
|
||||
| GET | `/ipv6/{direction}/{list_type}` | — | Get IPv6 list |
|
||||
| PUT | `/ipv6/{direction}/{list_type}` | `SocketAddrV6` JSON | Add entry |
|
||||
| DELETE | `/ipv6/{direction}/{list_type}` | `SocketAddrV6` JSON | Remove entry |
|
||||
|
||||
`direction`: `ingress` | `egress` — `list_type`: `whitelist` | `blacklist`
|
||||
|
||||
### eBPF Service Control — `/ebpf/service`
|
||||
|
||||
| Method | Path | Body | Description |
|
||||
|--------|------|------|-------------|
|
||||
| GET | `/ipv4/http_service` | — | List IPv4 HTTP service entries |
|
||||
| PUT | `/ipv4/http_service` | `[SocketAddrV4, [HttpMethod]]` | Add HTTP service |
|
||||
| DELETE | `/ipv4/http_service` | `[SocketAddrV4, [HttpMethod]]` | Remove HTTP service |
|
||||
| GET | `/ipv6/http_service` | — | List IPv6 HTTP service entries |
|
||||
| PUT | `/ipv6/http_service` | `[SocketAddrV6, [HttpMethod]]` | Add HTTP service |
|
||||
| DELETE | `/ipv6/http_service` | `[SocketAddrV6, [HttpMethod]]` | Remove HTTP service |
|
||||
| GET | `/ssh_white_list` | — | SSH whitelist enabled? |
|
||||
| POST | `/ssh_white_list/enable` | — | Enable SSH whitelist |
|
||||
| POST | `/ssh_white_list/disable` | — | Disable SSH whitelist |
|
||||
| GET/PUT/DELETE | `/ipv4/ssh_service` | `SocketAddrV4` | SSH service list |
|
||||
| GET/PUT/DELETE | `/ipv6/ssh_service` | `SocketAddrV6` | SSH service list |
|
||||
| GET/PUT/DELETE | `/ipv4/ssh_white_list` | `Ipv4Addr` | SSH whitelist |
|
||||
| GET/PUT/DELETE | `/ipv6/ssh_white_list` | `Ipv6Addr` | SSH whitelist |
|
||||
| GET/PUT/DELETE | `/ipv4/ssh_black_list` | `Ipv4Addr` | SSH blacklist |
|
||||
| GET/PUT/DELETE | `/ipv6/ssh_black_list` | `Ipv6Addr` | SSH blacklist |
|
||||
|
||||
### eBPF Statistics — `/ebpf/statistics`
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/get/ipv4/{direction}/{flow_direction}/{time_type}` | IPv4 flow data snapshot |
|
||||
| GET | `/get/ipv6/{direction}/{flow_direction}/{time_type}` | IPv6 flow data snapshot |
|
||||
| GET (WS) | `/websocket/ipv4/{direction}/{flow_direction}/{time_type}` | IPv4 flow stream |
|
||||
| GET (WS) | `/websocket/ipv6/{direction}/{flow_direction}/{time_type}` | IPv6 flow stream |
|
||||
|
||||
`direction`: `inbound` | `outbound` — `flow_direction`: `source` | `destination`
|
||||
`time_type`: `per_second` | `per_minute` | `per_hour`
|
||||
|
||||
### Health — `/health`
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/metrics` | Current system metrics (CPU, memory, etc.) |
|
||||
| GET | `/status` | Boolean system health |
|
||||
| GET (WS) | `/websocket/metrics` | Live metrics stream |
|
||||
|
||||
### Detection — `/detection`
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET (WS) | `/websocket/alert` | Unified alert stream (`UnifiedAlert` JSON) |
|
||||
|
||||
`UnifiedAlert` fields: `source`, `severity`, `src_ip`, `dst_ip`, `proto`, `timestamp`, `detail`
|
||||
|
||||
### Misc — `/misc`
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/boot_time` | System boot timestamp |
|
||||
|
||||
### WebSocket Protocol
|
||||
|
||||
All WebSocket endpoints push JSON on data events. Clients should:
|
||||
- Respond to `Ping` frames with `Pong` (handled automatically by most WS clients)
|
||||
- Send `Close` frame to disconnect gracefully
|
||||
|
||||
## HTTP Framework
|
||||
|
||||
axum 0.8 + tower-http. State is passed via `AppState` (Clone) registered with `.with_state()`.
|
||||
New handlers: add `State(state): State<AppState>` as parameter. Path params use `{param}` syntax.
|
||||
Auth-protected handlers: add `user: AuthenticatedUser` as parameter (auto-rejects invalid tokens).
|
||||
|
||||
```rust
|
||||
// Example: auth-protected handler
|
||||
async fn my_handler(
|
||||
user: AuthenticatedUser,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
// user.0.role, user.0.username available
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
See `TODO` for the full backlog.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user