* 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
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_traitmacro not needed — Rust 2024 edition supports async fn in traits natively- WebSocket: must
socket.split()intoSplitSink + SplitStreamfor 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_shutdownis synchronous to add but was previously missing, meaning SIGINT killed the process before eBPF detach could run.
Open Questions
- JWT secret rotation: no mechanism to rotate
jwt_secretwithout invalidating all active sessions. Acceptable for research prototype; production would need versioned keys or short TTL + refresh tokens. sessionstable 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.- Role enforcement:
AuthenticatedUser.roleis decoded from JWT but no handler currently gates on role. Future admin-only routes should checkuser.0.role == "admin". - 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/suppresscan now be auth-gated viaAuthenticatedUserextractor — add it to the route and it's protected. - XDP active response: when implemented,
POST /api/blacklistshould require auth.