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
This commit is contained in:
184
tasks/fix/admin-http-api.md
Normal file
184
tasks/fix/admin-http-api.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
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
|
||||
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:
|
||||
- `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, S1–S6 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
|
||||
80
tasks/fix/agents-md-project-structure.md
Normal file
80
tasks/fix/agents-md-project-structure.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
id: fix/agents-md-project-structure
|
||||
name: Update AGENTS.md project structure and common modifications after admin refactor
|
||||
status: open
|
||||
depends_on: [fix/admin-http-api]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: docs
|
||||
level: documentation
|
||||
review_findings: []
|
||||
adr: [028]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
After the admin socket → HTTP API migration, `AGENTS.md` needs updates to
|
||||
reflect the new project structure, config format, and operational procedures.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**Project Structure section** — Update to reflect new admin module layout:
|
||||
```
|
||||
src/
|
||||
├── admin/
|
||||
│ ├── auth.rs # Bearer token auth middleware (subtle, SHA-256)
|
||||
│ ├── handler.rs # HTTP handlers for /admin/reload, /status, /rotate-key
|
||||
│ └── mod.rs # Re-exports
|
||||
```
|
||||
Remove:
|
||||
```
|
||||
│ ├── socket.rs # REMOVED — was Unix domain socket admin API
|
||||
```
|
||||
|
||||
**Key Architecture Concepts section** — Update the admin socket description:
|
||||
- Replace "Unix domain socket (`admin_socket_path`)" with "Authenticated HTTP
|
||||
admin API (`admin_key_path`) on health check port"
|
||||
- Note that admin endpoints require Bearer token auth
|
||||
- Note that `admin_key_path` empty string = disabled (returns 404)
|
||||
|
||||
**Config Format section** — Update:
|
||||
- Replace `admin_socket_path` references with `admin_key_path`
|
||||
- Note that `admin_key_path` default is `/etc/reverse-proxy/admin-key`
|
||||
- Add key file format info (plaintext, one line, read once at startup)
|
||||
|
||||
**Common Modifications section** — Replace:
|
||||
```bash
|
||||
# Before (Unix socket)
|
||||
echo "reload" | socat - UNIX-CONNECT:/run/reverse-proxy/admin.sock
|
||||
|
||||
# After (HTTP with Bearer token)
|
||||
curl -H "Authorization: Bearer $ADMIN_KEY" http://127.0.0.1:9900/admin/reload
|
||||
curl -H "Authorization: Bearer $ADMIN_KEY" http://127.0.0.1:9900/admin/status
|
||||
```
|
||||
|
||||
**Build & Run section** — No changes needed (build commands unchanged).
|
||||
|
||||
**Testing section** — Note that admin tests now use HTTP (reqwest) instead of
|
||||
Unix socket (tokio::net::UnixStream).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Project structure shows `auth.rs` and `handler.rs`, not `socket.rs`
|
||||
- [ ] Key architecture concepts mention `admin_key_path` and Bearer token auth
|
||||
- [ ] Config format section mentions `admin_key_path`
|
||||
- [ ] Common modifications section uses `curl` examples, not `socat`
|
||||
- [ ] No references to `admin_socket_path` remain in AGENTS.md
|
||||
|
||||
## References
|
||||
|
||||
- AGENTS.md — current project structure and common modifications
|
||||
- docs/architecture/decisions/028-admin-http-api.md — ADR-028
|
||||
|
||||
## Notes
|
||||
|
||||
> Depends on `fix/admin-http-api` being complete so the new file names are
|
||||
> accurate.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
100
tasks/fix/config-reload-toctou.md
Normal file
100
tasks/fix/config-reload-toctou.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
id: fix/config-reload-toctou
|
||||
name: Add mtime check to config reload to detect mid-write file changes (ADR-029)
|
||||
status: open
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
review_findings: [W2]
|
||||
adr: [029]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Both the SIGHUP reload path (`src/shutdown.rs:handle_sighup_reload`) and the
|
||||
admin HTTP reload path (`src/admin/socket.rs:handle_reload`, soon
|
||||
`src/admin/handler.rs`) read the config file from disk with
|
||||
`tokio::fs::read_to_string()`, then parse and apply it. If another process is
|
||||
writing to the config file at the same time, the proxy could read a partially
|
||||
written config.
|
||||
|
||||
ADR-029 specifies a simple mitigation: compare file modification timestamps
|
||||
before and after reading. If mtime changed, reject the reload and return a
|
||||
"please retry" message.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**Shared reload function** — Extract the common file-read-and-validate logic
|
||||
from `src/shutdown.rs:handle_sighup_reload()` and
|
||||
`src/admin/socket.rs:handle_reload()` into a shared function (e.g.,
|
||||
`src/config/dynamic_config.rs` or a new `src/config/reload.rs`):
|
||||
|
||||
```rust
|
||||
pub async fn read_and_validate_config(
|
||||
config_path: &str,
|
||||
cli_allow_wildcard_bind: bool,
|
||||
) -> Result<(StaticConfig, DynamicConfig), ReloadError> {
|
||||
let metadata_before = tokio::fs::metadata(config_path).await
|
||||
.map_err(ReloadError::Io)?;
|
||||
let config_content = tokio::fs::read_to_string(config_path).await
|
||||
.map_err(ReloadError::Io)?;
|
||||
let metadata_after = tokio::fs::metadata(config_path).await
|
||||
.map_err(ReloadError::Io)?;
|
||||
|
||||
if metadata_before.modified().ok() != metadata_after.modified().ok() {
|
||||
return Err(ReloadError::FileChangedDuringRead);
|
||||
}
|
||||
|
||||
let full_config = FullConfig::parse(&config_content)?;
|
||||
let (new_static, new_dynamic) = full_config.into_static_and_dynamic();
|
||||
validate(&new_static, &new_dynamic, cli_allow_wildcard_bind)?;
|
||||
|
||||
Ok((new_static, new_dynamic))
|
||||
}
|
||||
```
|
||||
|
||||
**`src/shutdown.rs`** — Replace inline file read + parse + validate with a
|
||||
call to `read_and_validate_config()`. On `ReloadError::FileChangedDuringRead`,
|
||||
log a warning: "config file changed during read, please retry SIGHUP".
|
||||
|
||||
**`src/admin/handler.rs`** (after admin-http-api task) — Same call. On
|
||||
`ReloadError::FileChangedDuringRead`, return
|
||||
`{"status": "error", "message": "config file changed during read, please retry"}`.
|
||||
|
||||
**Error type** — Define `ReloadError` enum with variants:
|
||||
- `Io(std::io::Error)`
|
||||
- `Parse(toml::de::Error)`
|
||||
- `Validation(String)`
|
||||
- `FileChangedDuringRead`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Both SIGHUP and admin HTTP reload paths use the same file-reading logic
|
||||
- [ ] mtime is checked before and after reading the config file
|
||||
- [ ] If mtime changed, reload is rejected with a clear error message
|
||||
- [ ] Error message in admin HTTP response is generic ("config file changed
|
||||
during read, please retry") — no filesystem paths leaked
|
||||
- [ ] Full error details are logged server-side (path, mtime values)
|
||||
- [ ] SIGHUP path logs the same error at warn level
|
||||
- [ ] `cargo test` passes
|
||||
- [ ] `cargo clippy` passes with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/029-config-reload-toctou.md — ADR-029
|
||||
- docs/reviews/005-admin-socket-security-review.md — W2 finding
|
||||
- src/shutdown.rs — handle_sighup_reload
|
||||
- src/admin/socket.rs — handle_reload (to be replaced by admin/handler.rs)
|
||||
|
||||
## Notes
|
||||
|
||||
> This fix is independent of the admin socket → HTTP migration. It applies to
|
||||
> both reload paths (SIGHUP and admin). The implementation should be done
|
||||
> after or alongside the admin-http-api task since that task replaces
|
||||
> socket.rs with handler.rs.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
71
tasks/fix/review-005-status-update.md
Normal file
71
tasks/fix/review-005-status-update.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: fix/review-005-status-update
|
||||
name: Update security review #005 status to reflect ADR-028 decision
|
||||
status: open
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: docs
|
||||
level: documentation
|
||||
review_findings: [C1, C2, C3, W1, W3, W4, S1, S2, S3, S4, S5, S6]
|
||||
adr: [028]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Security review #005 (`docs/reviews/005-admin-socket-security-review.md`) is
|
||||
currently marked as `status: draft`. The review's architectural recommendation
|
||||
to replace the Unix domain socket with an authenticated HTTP admin endpoint has
|
||||
been accepted as ADR-028. The review findings should be annotated with their
|
||||
resolution status.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**`docs/reviews/005-admin-socket-security-review.md`**:
|
||||
- Update frontmatter `status` from `draft` to the appropriate post-decision
|
||||
status (e.g., `accepted` or `resolved`)
|
||||
- Add a resolution section at the top of the document noting:
|
||||
- C1, C2, C3, W1, W3, W4, S1–S6: **Resolved by ADR-028** (replacing Unix
|
||||
domain socket with authenticated HTTP admin API)
|
||||
- W2 (config file TOCTOU): **Tracked separately** — ADR-029, task
|
||||
`fix/config-reload-toctou`
|
||||
- W5 (wildcard flag inconsistency): **Tracked separately** — ADR-030, task
|
||||
`fix/wildcard-flag-reload`
|
||||
- W6 (changed_fields in reload response): **Tracked** — will be implemented
|
||||
as part of `fix/admin-http-api` (the new `/admin/reload` endpoint will
|
||||
include changed_fields in its response per operations.md)
|
||||
- W7 (health check port recon): **Accepted risk** — health check is
|
||||
localhost-only, returns minimal information. The admin HTTP endpoint adds
|
||||
authentication for `/admin/*` routes.
|
||||
|
||||
**`docs/reviews/006-attack-surface-review.md`**:
|
||||
- Update Category 5 (Admin Socket) references from `src/admin/socket.rs` to
|
||||
`src/admin/auth.rs` and `src/admin/handler.rs` (after admin-http-api task
|
||||
is complete)
|
||||
- Update entry 4.3 (admin reload config file) to reference the shared
|
||||
`read_and_validate_config()` function with mtime check
|
||||
- Remove or update entries that are eliminated by the socket removal (e.g.,
|
||||
Category 4: Unix Domain Socket entries)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Review #005 frontmatter status updated
|
||||
- [ ] Review #005 has a resolution section annotating each finding with its
|
||||
disposition (resolved by ADR-028, tracked separately, accepted risk)
|
||||
- [ ] Review #006 admin socket references updated (after admin-http-api task)
|
||||
- [ ] No inline content removed — findings are annotated, not deleted
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/005-admin-socket-security-review.md
|
||||
- docs/reviews/006-attack-surface-review.md
|
||||
- docs/architecture/decisions/028-admin-http-api.md
|
||||
|
||||
## Notes
|
||||
|
||||
> This task should be done after the `fix/admin-http-api` task is complete,
|
||||
> since review #006 references need to point to the new file structure.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
112
tasks/fix/wildcard-flag-reload.md
Normal file
112
tasks/fix/wildcard-flag-reload.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
id: fix/wildcard-flag-reload
|
||||
name: Store cli_allow_wildcard_bind in ConfigReloadHandle for consistent reload validation (ADR-030)
|
||||
status: open
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
review_findings: [W5]
|
||||
adr: [030]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
When the proxy starts with `--allow-wildcard-bind` (or `allow_wildcard_bind =
|
||||
true` in config), bind addresses using `0.0.0.0` are accepted. But on config
|
||||
reload, `validate()` is called with `cli_allow_wildcard_bind: false` — a
|
||||
hardcoded value in `ConfigReloadHandle::reload()`. This means a config that was
|
||||
valid at startup will be rejected on reload because the flag that enabled
|
||||
wildcard binding is not preserved.
|
||||
|
||||
ADR-030 specifies storing `cli_allow_wildcard_bind` in `ConfigReloadHandle` at
|
||||
construction time and using the stored value during reload validation.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**`src/config/dynamic_config.rs`** — `ConfigReloadHandle` struct:
|
||||
- Add `cli_allow_wildcard_bind: bool` field
|
||||
- Update `ConfigReloadHandle::new()` to accept and store the flag:
|
||||
```rust
|
||||
pub fn new(
|
||||
config: Arc<ArcSwap<DynamicConfig>>,
|
||||
static_config: StaticConfig,
|
||||
cli_allow_wildcard_bind: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
static_config: ArcSwap::from_pointee(static_config),
|
||||
reload_mutex: Mutex::new(()),
|
||||
cli_allow_wildcard_bind,
|
||||
}
|
||||
}
|
||||
```
|
||||
- In `reload()`, pass `self.cli_allow_wildcard_bind` to `validate()` instead
|
||||
of `false`:
|
||||
```rust
|
||||
validate(&new_static, &new_dynamic, self.cli_allow_wildcard_bind)?;
|
||||
```
|
||||
|
||||
**`src/main.rs`** — Update `ConfigReloadHandle::new()` call to pass
|
||||
`cli_allow_wildcard_bind` from the loaded config:
|
||||
```rust
|
||||
let reload_handle = Arc::new(ConfigReloadHandle::new(
|
||||
config_arc.clone(),
|
||||
loaded_config.static_config.clone(),
|
||||
loaded_config.cli_allow_wildcard_bind, // or args.allow_wildcard_bind
|
||||
));
|
||||
```
|
||||
|
||||
The `cli_allow_wildcard_bind` value should be the OR of the config flag and
|
||||
the CLI flag, matching the startup validation logic. Check `src/cli.rs` for
|
||||
how the flag is currently handled.
|
||||
|
||||
**`src/admin/socket.rs`** (or `src/admin/handler.rs` after migration) — Same
|
||||
change: pass the flag through to `ConfigReloadHandle::new()`.
|
||||
|
||||
**`src/config/validation.rs`** — No changes needed; `validate()` already
|
||||
accepts `cli_allow_wildcard_bind: bool` and uses it correctly.
|
||||
|
||||
**Tests** — Update all `ConfigReloadHandle::new()` calls to include the new
|
||||
parameter. Add a test that verifies:
|
||||
1. A config with `0.0.0.0` bind address is accepted on reload when
|
||||
`cli_allow_wildcard_bind: true`
|
||||
2. A config with `0.0.0.0` bind address is rejected on reload when
|
||||
`cli_allow_wildcard_bind: false`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ConfigReloadHandle` has a `cli_allow_wildcard_bind: bool` field
|
||||
- [ ] `ConfigReloadHandle::new()` accepts and stores `cli_allow_wildcard_bind`
|
||||
- [ ] `reload()` passes `self.cli_allow_wildcard_bind` to `validate()`
|
||||
(not hardcoded `false`)
|
||||
- [ ] All `ConfigReloadHandle::new()` call sites pass the correct flag
|
||||
- [ ] Config with `0.0.0.0` bind address is accepted on reload when flag is
|
||||
true (test)
|
||||
- [ ] Config with `0.0.0.0` bind address is rejected on reload when flag is
|
||||
false (test)
|
||||
- [ ] `cargo test` passes
|
||||
- [ ] `cargo clippy` passes with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/030-wildcard-flag-consistency.md — ADR-030
|
||||
- docs/reviews/005-admin-socket-security-review.md — W5 finding
|
||||
- docs/architecture/config.md — validation rules, allow_wildcard_bind
|
||||
- src/config/dynamic_config.rs — ConfigReloadHandle
|
||||
- src/config/validation.rs — validate()
|
||||
- src/cli.rs — CLI flag handling
|
||||
|
||||
## Notes
|
||||
|
||||
> This fix is independent of the admin socket → HTTP migration. It should be
|
||||
> applied to `ConfigReloadHandle` regardless of which admin interface is used.
|
||||
> The implementation is straightforward: add a field, pass it through.
|
||||
>
|
||||
> The flag value should be `allow_wildcard_bind || cli_allow_wildcard_bind`
|
||||
> (OR logic) matching the startup behavior documented in config.md.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user