refactor(gateway): remove the single-tenant data-plane paths (audit #400 finding 3) #402
Reference in New Issue
Block a user
Delete Branch "refactor/remove-single-tenant-data-plane"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Removes the legacy single-tenant (
resolver is None) data-plane paths from the shared gateway, closing finding #3 of the audit in #400. All three backends (docker, firecracker, macos-container) now launch through the consolidated orchestrator and every production gateway setsBOT_BOTTLE_ORCHESTRATOR_URL, so these branches were unreachable dead code — a second, security-relevant path to keep correct in parallel with the live one (finding #2 was exactly a case where one such path had rotted).Confirmed before cutting: the docker backend's
launch.pycallslaunch_consolidated()/teardown_consolidated()(its docstring was stale);orchestrator/gateway.py,firecracker/infra_vm.py, andmacos_container/infra.pyall set the orchestrator URL. No production gateway runs single-tenant.Scope (full teardown, one PR — as requested)
egress_addon.py— drops the static routes file entirely:EGRESS_ROUTES,_reload, the SIGHUP handler,self.config, and theSUPERVISE_BOTTLE_SLUGenv slug. The per-request/resolveis the only policy source;__init__fail-closes without an orchestrator URL. Introspection now reports the calling bottle's resolved routes. The block/redact log gates and_req_ctxredaction now read the per-flow config/env from the request-time stash (they silently used the empty static config before).git_http_backend.py— resolver mandatory, no flat-root fallback;main()refuses to start without an orchestrator URL; an unattributed source 404s.supervise_server.py— resolver mandatory; every proposal attributed to the source-IP-resolved bottle. Removeshandle_list_egress_routes(the proxy-fetch introspection that only worked when the proxy carried one bottle's identity);list-egress-routesis answered from the resolved policy.Safety
Nothing sends
docker kill --signal HUPto the gateway in the consolidated model — the egress applicators (docker + macOS) raise rather than signal — so removing the SIGHUP reload can't strand a signal that would now kill the egress process. Verified.Tests
A host-side fake resolver serves each test's
Configthrough the real parse path (a small YAML-subset emitter round-tripsroute_to_yaml_dict); the response/websocket hooks stash it asrequest()would. Deletes tests for the removed static-config / SIGHUP-reload / single-tenant-passthrough paths; adds fail-closed-without-orchestrator coverage for all three entry points (smoke-tested: git-http exits 1, supervise exits 2, egress__init__raises).test_gateway_initfailures on the dev host are the pre-existing/bin/sleep-on-NixOS issue, unrelated.Follow-ups (out of the data-plane teardown, noted in the commit)
gateway_initstill forwards SIGHUP to the egress child (now dormant — nothing sends it).🤖 Generated with Claude Code
https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
Review — remove the single-tenant data-plane paths
Verdict: approve. The teardown is coherent, genuinely fail-closed at every entry point, and the PR body's claims held up under verification. No blocking bugs found. (All 130 tests in the touched suites pass locally against
f26579c.)What I verified
orchestrator/gateway.py:226,firecracker/infra_vm.py:434,macos_container/infra.py:103all injectBOT_BOTTLE_ORCHESTRATOR_URL; the only no-URLDockerGateway()construction (docker/launch.py:148) just reads the CA from an already-running container, it never starts one.docker kill --signal HUPpath was removed in #385;gateway_init.py's forward is dormant as stated._block/_flow_logcall inrequest()comes after the_stash_flow_ctxategress_addon.py:347, so the new per-flow log level is always available; the_flow_ctxdeny-all fallback (Config(routes=()), log=LOG_OFF) makes hooks on unresolved flows a safe no-op._resolver_or_fail's_RpcInternalErroris caught and mapped by_dispatch's existing handler, so a misconfigured server returns a JSON-RPC error rather than crashing the thread._StaticResolver.resolve_policy_and_bottle_idmatches the realPolicyResolversignature, and the YAML-subset emitter drives the real parse path as claimed.serve()issupervise_server.main()— thebottle_slug→resolversignature change breaks nobody else.One belt-and-braces suggestion (non-blocking)
The egress fail-closed guarantee rests on mitmdump exiting when
addons = [EgressAddon()]raises at script load. mitmproxy 11.1.3'serrorcheckaddon does exit dump tools on startup errors, so this should hold — but the smoke test covered the raise itself, not mitmdump's reaction to it. If a future mitmproxy bump ever softened that to "log and continue", the failure mode is the worst one: a bare TLS-bumping open proxy with no policy. A two-line guard inegress_entrypoint.shbefore theexec mitmdump:makes the fail-closed property independent of mitmproxy's error handling.
Stale-comment fallout in untouched files (follow-up material)
orchestrator/gateway.py:129— "Empty → single-tenant." Now false: empty means all three daemons refuse to start. Could also just makeorchestrator_urla required kwarg now.egress_entrypoint.sh:18-19— "loads the addon that reads /etc/egress/routes.yaml" — that read no longer exists.Dockerfile.gateway:23— layout comment still lists/etc/egress/routes.yaml bind-mounted at run time.These fit naturally with the README follow-up already listed in the PR body.
Nice touches
_egress.local/allowlistnow shows only the calling bottle's resolved routes, and an unattributed caller sees an empty list rather than any global table._req_ctxredacting with the per-flow env overlay fixes a real (if quiet) gap — block logs previously scrubbed onlyos.environ, not the bottle's provisioned tokens.🤖 Review by Claude Code (Opus 4.8)
121c3e82ebtoec3b85ac1b99cc146cb8tod8e3947bd3