fix(sidecar_init): strip EGRESS_TOKEN_* from non-egress daemons' env (issue #84)
Pipelock was 403-blocking legitimate egress cred-injected traffic with 'blocked: request header contains secret'. The chain is `agent → egress → pipelock → internet`: egress injects `Authorization: Bearer <token>` for routes with an `auth_scheme`, then forwards upstream to pipelock. Pipelock has `scan_env: true` + `scan_headers: true` + `header_mode: all`, and the bundle supervisor spawned every daemon (egress, pipelock, git-gate, supervise) inheriting the bundle container's full env — including the `EGRESS_TOKEN_<n>` slots set via `docker run -e`. So pipelock had the token value egress injected sitting in its own env, matched it in the request headers, and blocked. The agent itself runs in a different machine and never sees `EGRESS_TOKEN_*`, so stripping these from non-egress daemons' env loses no DLP coverage — pipelock can't catch the exfil of a value the agent doesn't have in the first place. New helper `_env_for_daemon(name, base_env)` returns the unchanged base for `egress` and a copy with `EGRESS_TOKEN_*` filtered for everyone else. `_spawn` now passes the scoped env to `subprocess.Popen`. Prefix-based filter (not exact-match) so future egress-only env slots don't have to update this code. Tests: - `TestEnvForDaemon`: egress gets full env, pipelock / git-gate / supervise lose `EGRESS_TOKEN_0` + `EGRESS_TOKEN_1` but keep `PATH`, `EGRESS_UPSTREAM_PROXY`, `SUPERVISE_PORT`. - Independent-dict invariant locked so callers can't accidentally mutate the supervisor's env. 642 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #85.
This commit is contained in:
@@ -20,10 +20,56 @@ from unittest.mock import patch
|
||||
from claude_bottle.sidecar_init import (
|
||||
_DaemonSpec,
|
||||
_Supervisor,
|
||||
_env_for_daemon,
|
||||
_selected_daemons,
|
||||
)
|
||||
|
||||
|
||||
class TestEnvForDaemon(unittest.TestCase):
|
||||
"""Scope egress-only credential env vars to the egress daemon.
|
||||
|
||||
Regression for issue #84: pipelock's `scan_env: true` matched
|
||||
`EGRESS_TOKEN_*` against egress's just-injected Authorization
|
||||
header and 403-blocked the legitimate request. The agent
|
||||
never has access to these slots, so stripping them from
|
||||
non-egress daemons loses no DLP coverage."""
|
||||
|
||||
_BASE = {
|
||||
"PATH": "/usr/bin",
|
||||
"EGRESS_TOKEN_0": "tok-anthropic",
|
||||
"EGRESS_TOKEN_1": "tok-gitea",
|
||||
"EGRESS_UPSTREAM_PROXY": "http://127.0.0.1:8888",
|
||||
"SUPERVISE_PORT": "9100",
|
||||
}
|
||||
|
||||
def test_egress_sees_full_env(self):
|
||||
env = _env_for_daemon("egress", self._BASE)
|
||||
self.assertEqual(self._BASE, env)
|
||||
|
||||
def test_pipelock_loses_egress_tokens(self):
|
||||
env = _env_for_daemon("pipelock", self._BASE)
|
||||
self.assertNotIn("EGRESS_TOKEN_0", env)
|
||||
self.assertNotIn("EGRESS_TOKEN_1", env)
|
||||
# Non-token bundle env stays — supervise / git-gate / the
|
||||
# upstream proxy URL are all load-bearing for other
|
||||
# daemons.
|
||||
self.assertEqual("/usr/bin", env["PATH"])
|
||||
self.assertEqual("http://127.0.0.1:8888", env["EGRESS_UPSTREAM_PROXY"])
|
||||
self.assertEqual("9100", env["SUPERVISE_PORT"])
|
||||
|
||||
def test_git_gate_and_supervise_also_lose_egress_tokens(self):
|
||||
for name in ("git-gate", "supervise"):
|
||||
env = _env_for_daemon(name, self._BASE)
|
||||
self.assertNotIn("EGRESS_TOKEN_0", env)
|
||||
self.assertNotIn("EGRESS_TOKEN_1", env)
|
||||
|
||||
def test_returns_independent_dict(self):
|
||||
# Caller mutation mustn't affect the original.
|
||||
env = _env_for_daemon("pipelock", self._BASE)
|
||||
env["X"] = "y"
|
||||
self.assertNotIn("X", self._BASE)
|
||||
|
||||
|
||||
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