Files
reverse-proxy/tasks/fix/admin-http-api.md
glm-5.1 161049a17d Add ADR-029/030, implementation tasks, and spec updates for admin socket removal
Security review #005 identified critical vulnerabilities in the Unix domain
socket admin API (C1 symlink race, C2 no auth, C3 info leak, W1-W7, S1-S6).
ADR-028 (already accepted) replaces the socket with an authenticated HTTP
admin API on the health check port. This commit adds the remaining spec work:

- ADR-029: Config file TOCTOU mitigation (mtime check on reload)
- ADR-030: Store cli_allow_wildcard_bind in ConfigReloadHandle for consistent
  reload validation
- Implementation tasks for the admin HTTP migration (fix/admin-http-api),
  TOCTOU fix (fix/config-reload-toctou), and wildcard flag fix
  (fix/wildcard-flag-reload)
- Updated review #005 status to resolved with per-finding disposition
- Resolved OQ-16: POST for state-changing admin endpoints, GET for read-only
- Updated all architecture docs to reference new ADRs, use admin_key_path
  instead of admin_socket_path, and reflect POST method for /admin/reload
2026-06-15 05:19:42 +00:00

184 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: fix/admin-http-api
name: Replace Unix domain socket admin API with authenticated HTTP admin API (ADR-028)
status: open
depends_on: []
scope: broad
risk: high
impact: component
level: implementation
review_findings: [C1, C2, C3, W1, W3, W4, S1, S2, S3, S4, S5, S6]
adr: [028]
---
## 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
S1S6 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:
- `AdminAuthConfig` struct holding `Option<String>` for the SHA-256 hash of
the admin key (or `None` to disable admin endpoints)
- `admin_auth_middleware` axum middleware that validates `Authorization:
Bearer <token>` against the stored hash using `subtle::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. Returns `None` if path is empty (disabled). Logs a warning
and returns `None` if 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`.
Triggers `ConfigReloadHandle::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-export `AdminAuthConfig`, `AdminKeyError`,
`admin_auth_middleware`, `load_admin_key`, and the handler functions
- `src/health.rs` — expand `health_router()` to `admin_router()` that nests
admin routes under `/admin` with auth middleware. Merge into the health
check listener. The full router becomes:
```
/health → health_handler (GET, no auth)
/admin/* → auth middleware → admin handlers (POST for state-changing, GET for read-only)
```
The `start_health_check_listener` function signature changes to accept
`Option<Arc<AdminAuthConfig>>` and `Arc<ConfigReloadHandle>` and
`Arc<ArcSwap<[u8; 32]>>` for key rotation. If `AdminAuthConfig` is `None`,
`/admin/*` routes return 404.
- `src/main.rs` — remove admin socket initialization entirely (lines 102-127).
Add admin key loading step after config parsing:
```rust
let 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
};
```
Pass `admin_auth`, `reload_handle`, and `start_time` to
`start_health_check_listener`.
- `src/config/static_config.rs` — replace `admin_socket_path` field with
`admin_key_path`:
```rust
#[serde(default = "default_admin_key_path")]
pub admin_key_path: String,
```
Default: `"/etc/reverse-proxy/admin-key"`. Empty string disables admin
endpoints.
- `src/config/dynamic_config.rs` — `ConfigReloadHandle` gains
`cli_allow_wildcard_bind: bool` field (see task `fix/wildcard-flag-reload`).
No other changes needed — `reload()` method stays the same.
- `src/config/validation.rs` — add validation that `admin_key_path` is empty
or an absolute path (no `..` traversal, no relative paths). This is a new
validation rule.
- `Cargo.toml` — add `subtle` and `sha2` dependencies (already in overview.md)
**Tests:**
- Replace all `src/admin/socket.rs` tests with HTTP-based tests using
`reqwest` (already a dev dependency). Test:
- POST `/admin/reload` with valid Bearer token returns `{"status": "ok"}`
- POST `/admin/reload` with wrong token returns 401
- POST `/admin/reload` with no token returns 401
- POST `/admin/reload` when admin disabled returns 404
- GET `/admin/status` with valid token returns uptime and site count
- POST `/admin/rotate-key` with valid token returns new key and updates stored
hash
- POST `/admin/rotate-key` subsequent requests use the new key (old key returns
401)
- GET `/health` always returns 200 regardless of auth state
**Deployment:**
- `deploy/docker-compose.yml` — remove `/run/reverse-proxy` socket volume,
add `/etc/reverse-proxy/admin-key:/etc/reverse-proxy/admin-key:ro` volume
- `deploy/reverse-proxy.service` — remove any socket directory setup
- `deploy/README.md` — replace `socat` commands with `curl` examples
## Acceptance Criteria
- [ ] `src/admin/socket.rs` is deleted entirely
- [ ] `src/admin/auth.rs` implements Bearer token auth with constant-time
comparison and SHA-256 hashing
- [ ] `src/admin/handler.rs` implements `/admin/reload` (POST),
`/admin/status` (GET), `/admin/rotate-key` (POST)
- [ ] `src/health.rs` serves both `/health` (no auth) and `/admin/*`
(auth required) on port 9900
- [ ] `src/config/static_config.rs` uses `admin_key_path` (not
`admin_socket_path`)
- [ ] `src/main.rs` loads admin key at startup, passes auth config to
health check listener
- [ ] Admin disabled (`admin_key_path` empty 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 clippy` passes 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, S1S6 from security review #005.
>
> W2 (config TOCTOU) and W5 (wildcard flag) are independent fixes tracked in
> separate tasks.
>
> The `subtle` and `sha2` crates are already listed in the architecture spec
> (overview.md crate dependencies). Add them to `Cargo.toml` with appropriate
> versions.
## Summary
> To be filled on completion