feat(control-plane): role-scoped signed tokens so the gateway can't drive operator routes
test / integration-docker (pull_request) Successful in 17s
tracker-policy-pr / check-pr (pull_request) Successful in 16s
test / unit (pull_request) Successful in 39s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m21s
test / coverage (pull_request) Successful in 19s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (pull_request) Successful in 17s
tracker-policy-pr / check-pr (pull_request) Successful in 16s
test / unit (pull_request) Successful in 39s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m21s
test / coverage (pull_request) Successful in 19s
test / publish-infra (pull_request) Has been skipped
Review follow-up on #469: the data plane held the same control-plane secret that authorizes every route, so a compromised egress/git-gate could queue a supervise proposal AND approve it (or rewrite policy, read injected tokens) — the (source_ip, identity_token) checks attribute the *bottle*, not the caller. Replace the single shared bearer secret with role-scoped, HMAC-signed tokens (compact HS256 JWTs, stdlib-only — no new dependency): * new `control_auth` mints/verifies `{role}` tokens; roles are `gateway` (data plane) and `cli` (host operator/launcher). * the orchestrator holds only the signing *key* and verifies; `dispatch` gates each route by role — `gateway` reaches /resolve + /supervise/ {propose,poll}, everything else is `cli`-only (401 unauthenticated, 403 wrong role). * the gateway is handed a pre-minted `gateway` token it cannot rewrite into `cli`; the host CLI mints its own `cli` token from the host key. * `gateway_init` scopes the signing key to the orchestrator process and the gateway token to the data-plane daemons, so even in the combined infra container a compromised data-plane daemon never sees the key. Launchers (docker gateway + infra, macOS infra) inject the minted token(s); Firecracker stays open behind its nft boundary. Open mode (no key) still grants full `cli` access — the fail-visible fallback for tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,30 @@ class TestEnvForDaemon(unittest.TestCase):
|
||||
self.assertNotIn("X", self._BASE)
|
||||
|
||||
|
||||
class TestControlPlaneEnvScoping(unittest.TestCase):
|
||||
"""The control-plane signing key stays with the orchestrator; the pre-minted
|
||||
`gateway` JWT goes to the data-plane daemons (issue #469 review). Scoping
|
||||
them per-process keeps a compromised data-plane daemon from reading the key
|
||||
and minting a higher-privilege token, even in the combined infra container."""
|
||||
|
||||
_BASE = {
|
||||
"PATH": "/usr/bin",
|
||||
"BOT_BOTTLE_CONTROL_PLANE_TOKEN": "sk-x",
|
||||
"BOT_BOTTLE_CONTROL_AUTH_JWT": "gw-jwt",
|
||||
}
|
||||
|
||||
def test_orchestrator_gets_key_not_jwt(self):
|
||||
env = _env_for_daemon("orchestrator", self._BASE)
|
||||
self.assertEqual("sk-x", env["BOT_BOTTLE_CONTROL_PLANE_TOKEN"])
|
||||
self.assertNotIn("BOT_BOTTLE_CONTROL_AUTH_JWT", env)
|
||||
|
||||
def test_data_plane_daemons_get_jwt_not_key(self):
|
||||
for name in ("egress", "git-gate", "git-http", "supervise"):
|
||||
env = _env_for_daemon(name, self._BASE)
|
||||
self.assertNotIn("BOT_BOTTLE_CONTROL_PLANE_TOKEN", env, name)
|
||||
self.assertEqual("gw-jwt", env["BOT_BOTTLE_CONTROL_AUTH_JWT"], name)
|
||||
|
||||
|
||||
class TestSelectedDaemons(unittest.TestCase):
|
||||
"""Env-var subset filtering. The compose renderer is the source
|
||||
of truth for which daemons are wired; the supervisor just
|
||||
|
||||
Reference in New Issue
Block a user