Mantis/.research/findings/tasks/acc-001-c1.md
ParrotXray 5e146717d9
Fix/some issus (#14)
* wip

* feat: Add egress eBPF access control and fix Suricata HTTP port detection

* feat: adjust code with rustfmt

* feat: adjust code with rustfmt

* feat: Remove SERVICE stage from ingress eBPF pipeline
2026-05-23 16:35:27 +08:00

5.0 KiB

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?

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?

{
  "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?

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.