8.1 KiB
id, name, status, depends_on, scope, risk, impact, level, review_findings, adr
| id | name | status | depends_on | scope | risk | impact | level | review_findings | adr | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix/admin-http-api | Replace Unix domain socket admin API with authenticated HTTP admin API (ADR-028) | pending | broad | high | component | implementation |
|
|
Description
Replace the Unix domain socket admin API (src/admin/socket.rs) with
authenticated HTTP endpoints on the existing health check listener. This
eliminates the entire class of filesystem-based vulnerabilities identified in
security review #005 (C1 symlink race, C2 no authentication, C3 info leak, W1
no concurrency limit, W3 path validation, W4 is_socket_active side effect, and
S1–S6 suggestions).
ADR-028 defines the replacement design. The health check listener on
127.0.0.1:9900 already runs an axum router. Admin endpoints are added behind
Bearer token authentication middleware.
Changes Required
Remove:
src/admin/socket.rs— entire file (826 lines of Unix socket code)src/admin/mod.rs— current re-exports (AdminSocket,AdminSocketError,start_admin_socket)
Add:
src/admin/auth.rs— Bearer token middleware:AdminAuthConfigstruct holdingOption<String>for the SHA-256 hash of the admin key (orNoneto disable admin endpoints)admin_auth_middlewareaxum middleware that validatesAuthorization: Bearer <token>against the stored hash usingsubtle::ConstantTimeEq- Returns 404 when admin is disabled (empty
admin_key_path), 401 on missing/wrong token, passes through on valid token load_admin_key(path: &str) -> Result<Option<[u8; 32]>, AdminKeyError>function that reads the key file, hashes it with SHA-256, and returns the hash. ReturnsNoneif path is empty (disabled). Logs a warning and returnsNoneif the file doesn't exist or is unreadable (admin endpoints disabled, process continues starting).
src/admin/handler.rs— HTTP handlers:reload_handler(State<...>) -> Json<ReloadResponse>— POST/admin/reload. TriggersConfigReloadHandle::reload(), returns{"status": "ok"}or{"status": "error", "message": "reload failed"}. Generic error messages only; details logged server-side.status_handler(State<...>) -> Json<StatusResponse>— GET/admin/status. Returns{"status": "ok", "uptime_secs": N, "sites": N}rotate_key_handler(State<...>) -> Json<RotateKeyResponse>— POST/admin/rotate-key. Generates new 256-bit random key, returns plaintext in response, replaces stored hash in memory. Returns{"status": "ok", "key": "<hex>"}.
Modify:
src/admin/mod.rs— re-exportAdminAuthConfig,AdminKeyError,admin_auth_middleware,load_admin_key, and the handler functionssrc/health.rs— expandhealth_router()toadmin_router()that nests admin routes under/adminwith auth middleware. Merge into the health check listener. The full router becomes:The/health → health_handler (GET, no auth) /admin/* → auth middleware → admin handlers (POST for state-changing, GET for read-only)start_health_check_listenerfunction signature changes to acceptOption<Arc<AdminAuthConfig>>andArc<ConfigReloadHandle>andArc<ArcSwap<[u8; 32]>>for key rotation. IfAdminAuthConfigisNone,/admin/*routes return 404.src/main.rs— remove admin socket initialization entirely (lines 102-127). Add admin key loading step after config parsing:Passlet admin_auth = if !static_config.admin_key_path.is_empty() { match admin::load_admin_key(&static_config.admin_key_path) { Ok(Some(hash)) => Some(Arc::new(AdminAuthConfig { admin_key_hash: hash })), Ok(None) => None, // disabled Err(e) => { warn!("admin key load failed, disabling admin endpoints: {}", e); None } } } else { None };admin_auth,reload_handle, andstart_timetostart_health_check_listener.src/config/static_config.rs— replaceadmin_socket_pathfield withadmin_key_path:Default:#[serde(default = "default_admin_key_path")] pub admin_key_path: String,"/etc/reverse-proxy/admin-key". Empty string disables admin endpoints.src/config/dynamic_config.rs—ConfigReloadHandlegainscli_allow_wildcard_bind: boolfield (see taskfix/wildcard-flag-reload). No other changes needed —reload()method stays the same.src/config/validation.rs— add validation thatadmin_key_pathis empty or an absolute path (no..traversal, no relative paths). This is a new validation rule.Cargo.toml— addsubtleandsha2dependencies (already in overview.md)
Tests:
- Replace all
src/admin/socket.rstests with HTTP-based tests usingreqwest(already a dev dependency). Test:- POST
/admin/reloadwith valid Bearer token returns{"status": "ok"} - POST
/admin/reloadwith wrong token returns 401 - POST
/admin/reloadwith no token returns 401 - POST
/admin/reloadwhen admin disabled returns 404 - GET
/admin/statuswith valid token returns uptime and site count - POST
/admin/rotate-keywith valid token returns new key and updates stored hash - POST
/admin/rotate-keysubsequent requests use the new key (old key returns 401) - GET
/healthalways returns 200 regardless of auth state
- POST
Deployment:
deploy/docker-compose.yml— remove/run/reverse-proxysocket volume, add/etc/reverse-proxy/admin-key:/etc/reverse-proxy/admin-key:rovolumedeploy/reverse-proxy.service— remove any socket directory setupdeploy/README.md— replacesocatcommands withcurlexamples
Acceptance Criteria
src/admin/socket.rsis deleted entirelysrc/admin/auth.rsimplements Bearer token auth with constant-time comparison and SHA-256 hashingsrc/admin/handler.rsimplements/admin/reload(POST),/admin/status(GET),/admin/rotate-key(POST)src/health.rsserves both/health(no auth) and/admin/*(auth required) on port 9900src/config/static_config.rsusesadmin_key_path(notadmin_socket_path)src/main.rsloads admin key at startup, passes auth config to health check listener- Admin disabled (
admin_key_pathempty or file missing) →/admin/*returns 404 - Wrong/missing Bearer token → 401
- Error responses are generic (no filesystem paths, no config details)
- Full error details logged server-side only
- Key rotation works in-memory (new key replaces stored hash, old key rejected)
- Key rotation does not persist across restarts (documented behavior)
- SIGHUP reload continues to work unchanged
- All existing tests pass (minus deleted socket tests)
- New HTTP-based admin tests pass
cargo clippypasses with no warnings- Deployment files updated (docker-compose, systemd, README)
References
- docs/architecture/decisions/028-admin-http-api.md — ADR-028
- docs/architecture/decisions/014-unix-socket-reload.md — superseded ADR
- docs/architecture/decisions/027-admin-socket-resource-limits.md — deprecated
- docs/architecture/operations.md — admin HTTP endpoint, key management
- docs/architecture/config.md — admin_key_path, StaticConfig
- docs/architecture/overview.md — crate dependencies, architecture diagram
- docs/reviews/005-admin-socket-security-review.md — C1, C2, C3, W1, W3, W4
- src/admin/socket.rs — code to remove
- src/health.rs — code to extend
- src/main.rs — admin socket init to remove/replace
- src/config/static_config.rs — field rename
Notes
This is the primary implementation task for the admin socket → HTTP API migration. It directly implements ADR-028 and resolves findings C1, C2, C3, W1, W3, W4, S1–S6 from security review #005.
W2 (config TOCTOU) and W5 (wildcard flag) are independent fixes tracked in separate tasks.
The
subtleandsha2crates are already listed in the architecture spec (overview.md crate dependencies). Add them toCargo.tomlwith appropriate versions.
Summary
To be filled on completion