fix(#19): gate /vault/:secret behind bearer token + bind loopback by default#39
Conversation
… default PR #19 review surfaced that GET /vault/:secret resolves a real secret with no authn — and the binary binds 0.0.0.0, so anyone on the LAN could enumerate names. Fix two layers: 1. router.rs — handler now requires `Authorization: Bearer <token>` matching `SECURITY_PROXY_VAULT_TOKEN` env. Unset env → route returns 503 (refuses to act as an oracle); wrong/missing bearer → 401. Constant-time bearer comparison. 2. main.rs — bind defaults to 127.0.0.1; operators who need remote access set SECURITY_PROXY_BIND=0.0.0.0 explicitly. vault_route tests updated for the bearer requirement; two new tests cover the 503-when-token-unset and 401-when-bearer-missing paths.
There was a problem hiding this comment.
Pull request overview
This PR hardens security-proxy by restricting access to the /vault/:secret endpoint and reducing default network exposure, addressing a high-priority “unauthenticated secret oracle” risk.
Changes:
- Add bearer-token auth gating for
GET /vault/:secretcontrolled bySECURITY_PROXY_VAULT_TOKEN, including 503 when unset and 401 on missing/wrong auth. - Default the binary to bind loopback-only (
127.0.0.1) unless overridden viaSECURITY_PROXY_BIND. - Update and extend
/vault/:secretintegration tests to cover the new auth/disabled behaviors.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| crates/security-proxy/src/router.rs | Adds env-driven bearer auth gate for /vault/:secret and related helper(s). |
| crates/security-proxy/src/main.rs | Switches default bind host to 127.0.0.1 with SECURITY_PROXY_BIND override. |
| crates/security-proxy/tests/vault_route.rs | Updates existing vault-route tests for auth and adds new 503/401 cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if a.len() != b.len() { | ||
| return false; | ||
| } | ||
| let mut diff = 0u8; | ||
| for (x, y) in a.iter().zip(b.iter()) { | ||
| diff |= x ^ y; | ||
| } |
There was a problem hiding this comment.
constant_time_eq returns early when lengths differ, which defeats the stated goal of avoiding length/prefix timing leaks (an attacker can distinguish “right length” vs “wrong length” by timing). Consider making the comparison run in constant time regardless of length (e.g., include len(a) ^ len(b) in the diff and iterate over max_len), or use a well-audited constant-time compare implementation.
| if a.len() != b.len() { | |
| return false; | |
| } | |
| let mut diff = 0u8; | |
| for (x, y) in a.iter().zip(b.iter()) { | |
| diff |= x ^ y; | |
| } | |
| let max_len = a.len().max(b.len()); | |
| let mut diff = (a.len() ^ b.len()) as u8; | |
| for i in 0..max_len { | |
| let x = a.get(i).copied().unwrap_or(0); | |
| let y = b.get(i).copied().unwrap_or(0); | |
| diff |= x ^ y; | |
| } |
There was a problem hiding this comment.
Codex integration sweep: acknowledged. I am not editing this branch per the parallel-agent boundary; this looks actionable for the PR owner because the current helper still leaks length timing despite the constant-time wording.
| let provided = headers | ||
| .get(axum::http::header::AUTHORIZATION) | ||
| .and_then(|v| v.to_str().ok()) | ||
| .and_then(|v| v.strip_prefix("Bearer ")) | ||
| .unwrap_or(""); |
There was a problem hiding this comment.
Authorization scheme names are case-insensitive per RFC 7235; using strip_prefix("Bearer ") will reject valid authorization: bearer <token> headers. Consider parsing the scheme case-insensitively (and being a bit more tolerant of whitespace) to avoid surprising client failures.
There was a problem hiding this comment.
Codex integration sweep: acknowledged. I am not editing this branch; this is a good compatibility fix for the PR owner because Bearer auth schemes should be parsed case-insensitively.
| // Default to loopback-only because the router exposes a | ||
| // `GET /vault/:secret` endpoint that resolves to a real token. Even | ||
| // with the bearer-token guard the router added, having the binary | ||
| // bind 0.0.0.0 by default put the entire LAN one network hop from a | ||
| // secret-exfil attempt. Operators who need remote access set | ||
| // SECURITY_PROXY_BIND=0.0.0.0 explicitly (and should pair it with | ||
| // SECURITY_PROXY_VAULT_TOKEN, which gates the vault route). | ||
| let bind_host = std::env::var("SECURITY_PROXY_BIND").unwrap_or_else(|_| "127.0.0.1".into()); | ||
| let addr = format!("{}:{}", bind_host, port); |
There was a problem hiding this comment.
SECURITY_PROXY_BIND is introduced here but the config docs currently only mention SECURITY_PROXY_PORT (see GatewayConfig docs). To reduce operator confusion, consider documenting SECURITY_PROXY_BIND alongside the other env overrides (or moving bind host/addr into config).
There was a problem hiding this comment.
Codex integration sweep: acknowledged. I am not editing this branch; documenting SECURITY_PROXY_BIND alongside SECURITY_PROXY_PORT is a sensible operator-facing follow-up for this PR.
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Subsumed by #44 (squashed to |
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Address triage LEGIT-FIX from PR #19 review:
GET /vault/:secretnow requiresAuthorization: Bearer <token>matching the newSECURITY_PROXY_VAULT_TOKENenv var.127.0.0.1; operators setSECURITY_PROXY_BIND=0.0.0.0explicitly when remote access is needed.This is the highest-priority security finding from the cross-PR triage — without it, the proxy on the default config exposed an unauthenticated secret oracle to anyone on the LAN.
Test plan
cargo test -p security-proxy --test vault_route— all 4 pass (2 existing tests updated for new auth, 2 new tests for 503-when-token-unset and 401-when-bearer-missing)Notes for follow-up
integration/all-code-changesdirectly.🤖 Generated with Claude Code