2026-06-25: The gateway gets a doorman — proxyd's api-key gate + a lint that won't blink

The wall had a door, and the door had no lock. The loopback surface sweep closed the network around proxyd — the model gateway now binds 127.0.0.1 and nothing else. But that doc named a gap it did not yet close: proxyd does not validate its own api-key. It reads the x-api-key header off an inbound request and forwards it upstream, and it never checks whether the caller was allowed to use the gateway at all. Loopback shuts the network door. It does nothing about a local process that simply opens it. Windu, who runs security, called it the doorman that was missing. This is that doorman.
The doorman: inbound x-api-key validation
Section titled “The doorman: inbound x-api-key validation”proxyd now runs an axum middleware that checks the inbound x-api-key before any route handler sees the request. It mirrors the pattern the :1337/:3301 machine backends already use: a constant-time comparison (subtle::ConstantTimeEq, so a wrong key takes the same time to reject as a near-miss), fail-closed — anything that is not an exact match of an accepted key gets a 401, full stop. No exceptions. No fuzzy match.
This is defense-in-depth, not the primary control. proxyd is loopback-only. The threat model is a rogue process already on the box, not the LAN — the same threat the mTLS migration hardened the machine backends against. The loopback bind is the wall. The api-key is the lock on the one door through it.
When the time comes to enforce, the rollout is dual-accept so nothing breaks mid-transition. The agents’ built-in key sanctum-local is always accepted; additional keys come from SANCTUM_PROXY_INBOUND_KEYS (comma-separated) and SANCTUM_PROXY_KEY. The order matters. Enumerate every real caller’s key into the accept set, set it live, then flip SANCTUM_PROXY_ENFORCE_AUTH=true, watch the log for inbound_auth=ENFORCING, and confirm the council and CLI still get 200s. /health stays exempt so monitoring probes never need a key.
The tests do not re-implement the compare loop and grade their own homework. They drive the real middleware through an axum router (tower oneshot): enforce-off is a genuine no-op, the dual-accept set passes either key, wrong / missing / length-off keys all 401, and /health passes unauthenticated even while enforcing.
The lint that won’t blink
Section titled “The lint that won’t blink”The same pass made the whole Rust workspace pass cargo clippy --all-targets -- -D warnings, and flipped the CI lint job from a tolerant report to a hard gate. One warning now fails the build. Test code included.
Getting there meant triaging a real dead-code backlog rather than papering over it. The rule was simple. Delete what is genuinely dead, and for code that is intentional but not yet wired, leave an explicit #[allow(...)] with a reason at the site — never a blanket module-wide silence. That is the difference between a clean report and an honest one.
- Deleted: the watchdog daemon’s retired Idle Manager — a TCP proxy that used to lazily spawn a council backend, dead since council-mlx moved to mTLS-only on its own port. Gone, along with its now-orphaned imports.
- Allowed, with rationale: the audio-provenance signing module (a complete Ed25519 API shared across two binaries that each use a different half), metrics accessors, and an arg-heavy set of provider-forwarding functions where a context struct would add indirection without removing a field.
- Fixed: a
not_unsafe_ptr_arg_dereferror in the castellan memory-pressure FFI that had been hiding behind the dead-code noise — the function that hands a raw pointer to libdispatch is now correctlyunsafe fnwith a documented safety contract, and its one caller wraps it inunsafe.
Two warts the verification turned up
Section titled “Two warts the verification turned up”Final verification is where you find the things nobody meant to ship. Two surfaced. Both benign, both worth recording. The first was the not_unsafe_ptr_arg_deref bug above, hiding in the dead-code noise until the lint gate flushed it out. The second was hiding in plain sight, in the process table.
sanctumd (the watchdog API on :2187) is loaded twice under one launchd label — once as a system LaunchDaemon and once as a user LaunchAgent. Both run the same binary, whose default is to bind loopback plus the tailnet; they race, so one process wins the loopback listener and the other wins the tailnet one. The posture is still correct — loopback and tailnet, never 0.0.0.0 — so this is a topology wart, not an exposure. The fix keeps the canonical system daemon (the one rendered from watchdog.yaml) and retires the stray LaunchAgent.
The doctrine
Section titled “The doctrine”A closed network door and an unlocked door behind it is one control, not two. The sweep gave proxyd the wall; this gives it the lock — shipped cold so the council never feels it, ready to arm the day a local-process threat is worth arming against. The lint gate is the same idea pointed inward: the standard only holds if a single new warning is enough to stop the line.
Tommy patrolled a haus whose doors were all locked, twice a day, for fifteen years. He never once turned a knob to check. He simply knew the difference between a door that is shut and a door that is held. So does proxyd, now.