feat(gateway): mandatory identity-token attribution on every data plane (PR #354 review)
lint / lint (push) Successful in 2m12s
test / unit (pull_request) Successful in 1m12s
test / integration (pull_request) Successful in 27s
test / coverage (pull_request) Successful in 1m21s

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:
2026-07-16 17:02:41 -04:00
parent 914f01fa8f
commit d4b27ebf1f
16 changed files with 177 additions and 27 deletions
@@ -74,6 +74,19 @@ class TestRenderGitconfig(unittest.TestCase):
out = git_gate_render_gitconfig((_entry(),), "1.2.3.4:9418", scheme="http")
self.assertIn('[url "http://1.2.3.4:9418/repo.git"]', out)
def test_identity_token_extraheader_over_http(self) -> None:
# Delivered as a URL-scoped http.extraHeader so git-http can enforce
# the mandatory (source_ip, token) pair; only over the http transport.
out = git_gate_render_gitconfig(
(_entry(),), "1.2.3.4:9420", scheme="http", identity_token="TOK123")
self.assertIn('[http "http://1.2.3.4:9420/"]', out)
self.assertIn("extraHeader = x-bot-bottle-identity: TOK123", out)
def test_identity_token_omitted_over_git_scheme(self) -> None:
out = git_gate_render_gitconfig(
(_entry(),), "git-gate", scheme="git", identity_token="TOK123")
self.assertNotIn("extraHeader", out)
def test_remote_key_alias_with_nondefault_port(self) -> None:
out = git_gate_render_gitconfig(
(_entry(RemoteKey="10.0.0.5", UpstreamPort="2222"),), "git-gate",
+15 -3
View File
@@ -171,14 +171,26 @@ class TestDispatch(unittest.TestCase):
)
self.assertEqual(400, status)
def test_resolve_without_token_by_source_ip(self) -> None:
_, reg = dispatch(
def test_resolve_without_token_denies(self) -> None:
# Mandatory token: source-IP alone no longer resolves (fail-closed 403).
dispatch(
self.orch, "POST", "/bottles",
_body({"source_ip": "10.243.0.5", "policy": "P"}),
)
status, payload = dispatch(
status, _ = dispatch(
self.orch, "POST", "/resolve", _body({"source_ip": "10.243.0.5"})
)
self.assertEqual(403, status)
def test_resolve_with_matching_token(self) -> None:
_, reg = dispatch(
self.orch, "POST", "/bottles",
_body({"source_ip": "10.243.0.6", "policy": "P"}),
)
status, payload = dispatch(
self.orch, "POST", "/resolve",
_body({"source_ip": "10.243.0.6", "identity_token": reg["identity_token"]}),
)
self.assertEqual(200, status)
self.assertEqual(reg["bottle_id"], payload["bottle_id"])
self.assertEqual("P", payload["policy"])
+5 -2
View File
@@ -114,9 +114,12 @@ class TestOrchestrator(unittest.TestCase):
def test_set_policy_unknown_is_false(self) -> None:
self.assertFalse(self.orch.set_policy("ghost", "{}"))
def test_resolve_by_source_ip_without_token(self) -> None:
def test_resolve_requires_matching_token(self) -> None:
# Mandatory (source_ip, token) pair — no source-IP-only fallback.
rec = self.orch.launch_bottle("10.243.0.1", policy="P")
got = self.orch.resolve("10.243.0.1") # network-layer, no token
self.assertIsNone(self.orch.resolve("10.243.0.1", "")) # empty token denies
self.assertIsNone(self.orch.resolve("10.243.0.1", "wrong")) # mismatch denies
got = self.orch.resolve("10.243.0.1", rec.identity_token) # exact pair
assert got is not None
self.assertEqual(rec.bottle_id, got.bottle_id)
self.assertEqual("P", got.policy)