fix(egress-proxy): force traffic through pipelock + block unallowlisted hosts
Two issues stopping the bottle's egress allowlist from being enforced: 1. mitmproxy was bypassing pipelock. We set HTTPS_PROXY=pipelock in the egress-proxy container's env, but mitmproxy is a proxy *server* — it does NOT honor HTTP(S)_PROXY env vars on its outbound side the way HTTP-client libraries do. All post-MITM traffic was going direct to the upstream, never touching pipelock's hostname allowlist or DLP scanner. Fix: use mitmproxy's `--mode upstream:URL` flag. The Dockerfile entrypoint now reads a new `EGRESS_PROXY_UPSTREAM_PROXY` env (set by `DockerEgressProxy.start` to the pipelock URL when pipelock is in the topology) and switches mitmdump to upstream-proxy mode. Standalone runs of the image without the env still get `--mode regular@9099` direct-to-upstream — useful for unit-test boots. Confirmed in the boot log: "HTTP(S) proxy (upstream mode) listening at *:9099." 2. egress-proxy was forwarding unrecognized hosts. The addon's `decide()` returned `Decision(action="forward")` whenever no route matched the request host, deferring to pipelock to gate. With #1 broken pipelock wasn't gating either; even with #1 fixed, defense-in-depth wants both layers enforcing. Fix: no-route-match → 403 with a "host not in allowlist" reason. The egress allowlist is now strictly the set of hosts declared in `bottle.egress_proxy.routes`; bare-pass routes (host with no auth, no path_allowlist) cover the passthrough case for hosts that just need reach. path_allowlist enforcement on matched routes is unchanged. Test updated: `test_no_matching_route_forwards` → `test_no_matching_route_blocks`. 364 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -152,13 +152,15 @@ class TestMatchRoute(unittest.TestCase):
|
||||
|
||||
|
||||
class TestDecide(unittest.TestCase):
|
||||
def test_no_matching_route_forwards(self):
|
||||
# Hostnames the operator didn't declare are not the
|
||||
# egress-proxy's concern; pipelock's hostname allowlist gates
|
||||
# them downstream.
|
||||
def test_no_matching_route_blocks(self):
|
||||
# Defense-in-depth: egress-proxy gates the bottle's allowlist
|
||||
# too, not just pipelock. Any host the operator didn't declare
|
||||
# in egress_proxy.routes is 403'd at egress-proxy before it
|
||||
# ever reaches pipelock.
|
||||
d = decide((), "elsewhere.example", "/anything", {})
|
||||
self.assertEqual("forward", d.action)
|
||||
self.assertIsNone(d.inject_authorization)
|
||||
self.assertEqual("block", d.action)
|
||||
self.assertIn("allowlist", d.reason)
|
||||
self.assertIn("'elsewhere.example'", d.reason)
|
||||
|
||||
def test_path_allowlist_match_forwards(self):
|
||||
d = decide(
|
||||
|
||||
Reference in New Issue
Block a user