feat(gateway): mandatory identity-token attribution on every data plane (PR #354 review)
Codex review: the /31 TAP doesn't make source IP unspoofable, and the app-layer token was returned by launch but never delivered or enforced, so a spoofed source could select a victim bottle's policy/tokens. Make the token mandatory and deliver it on each attributed plane (anti-spoof landed separately as the network boundary). Enforcement (control plane): - `Orchestrator.resolve` now requires a matching (source_ip, identity_token) pair (constant-time) — no source-IP-only fallback. `/resolve` fail-closes (403) on a missing/empty/mismatched token. Delivery, per plane (the token is `token_urlsafe`, safe in a URL): - egress: proxy credentials (`HTTPS_PROXY=http://bottle:<token>@gw`). The addon reads `Proxy-Authorization` — from the request (HTTP) or captured at the CONNECT for HTTPS tunnels (keyed by client conn, cleared on disconnect) — validates, and strips it (+ the legacy header) before upstream. - git-http: a URL-scoped `http.<gate>/.extraHeader: x-bot-bottle-identity` in the agent's git config (only over the http transport). - supervise: `mcp add --header x-bot-bottle-identity: <token>` (claude + codex); the server reads the header and passes it to resolve. Wiring: thread `ctx.identity_token` onto the firecracker + docker plans and into the agent env/config at launch. Verified on a KVM host: egress with the correct proxy-cred token returns 200 (HTTP and HTTPS/CONNECT), and no-token / wrong-token return 403; a real `cli.py start --backend=firecracker` launch provisions git config + the supervise MCP header and reaches the agent session, all under mandatory enforcement. Fixed a `claude mcp add` arg-order bug (--header must follow the positional name/url) found by that launch. Transparent proxy for tools that ignore proxy env is deferred to a follow-up (see thread); anti-spoof + host firewall remain the fail-closed boundary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -18,6 +18,10 @@ class FirecrackerBottlePlan(BottlePlan):
|
||||
agent_proxy_url: str = ""
|
||||
agent_git_gate_url: str = ""
|
||||
agent_supervise_url: str = ""
|
||||
# Per-bottle identity token the agent presents on every attributed request
|
||||
# (egress proxy credentials, git-gate/supervise headers); set by launch
|
||||
# from the orchestrator registration. Empty pre-registration.
|
||||
identity_token: str = ""
|
||||
|
||||
@property
|
||||
def container_name(self) -> str:
|
||||
|
||||
@@ -147,7 +147,15 @@ def launch(
|
||||
plan,
|
||||
git_gate_plan=git_gate_plan,
|
||||
egress_plan=egress_plan,
|
||||
agent_proxy_url=f"http://{slot.host_ip}:{EGRESS_PORT}",
|
||||
identity_token=ctx.identity_token,
|
||||
# Deliver the identity token as egress proxy credentials — clients
|
||||
# honor `HTTPS_PROXY=http://id:token@gw` without app changes; the
|
||||
# gateway reads Proxy-Authorization, validates the (source_ip,
|
||||
# token) pair, and strips it before upstream.
|
||||
agent_proxy_url=(
|
||||
f"http://bottle:{ctx.identity_token}"
|
||||
f"@{slot.host_ip}:{EGRESS_PORT}"
|
||||
),
|
||||
agent_git_gate_url=git_gate_url,
|
||||
agent_supervise_url=supervise_url,
|
||||
)
|
||||
@@ -226,7 +234,8 @@ def _agent_guest_env(plan: FirecrackerBottlePlan, host_ip: str) -> dict[str, str
|
||||
"""Env injected into every agent/exec call over SSH. The VM has no
|
||||
baked process env (it just runs init), so the proxy/CA/git/supervise
|
||||
wiring is applied per-invocation."""
|
||||
proxy_url = f"http://{host_ip}:{EGRESS_PORT}"
|
||||
# Carries the identity token as proxy credentials (set in `launch`).
|
||||
proxy_url = plan.agent_proxy_url or f"http://{host_ip}:{EGRESS_PORT}"
|
||||
no_proxy = f"localhost,127.0.0.1,{host_ip}"
|
||||
env: dict[str, str] = {
|
||||
"HTTPS_PROXY": proxy_url, "HTTP_PROXY": proxy_url,
|
||||
|
||||
Reference in New Issue
Block a user