Commit Graph

4 Commits

Author SHA1 Message Date
didericis f807ed1149 fix(egress-proxy): force traffic through pipelock + block unallowlisted hosts
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m5s
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>
2026-05-25 16:38:18 -04:00
didericis b9c70f7daa fix(egress-proxy): build combined trust bundle (system + pipelock CA)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m2s
`--set ssl_verify_upstream_trusted_ca` REPLACES mitmproxy's default
trust store with the file we point it at. The earlier wiring
pointed it at just pipelock's CA, which broke for any host pipelock
passes through (api.anthropic.com is in DEFAULT_TLS_PASSTHROUGH):
pipelock CONNECT-tunnels the handshake to the real upstream,
egress-proxy sees the real public cert (signed by e.g. DigiCert),
and refuses to validate because pipelock's CA doesn't sign it.

Fix in Dockerfile entrypoint: when EGRESS_PROXY_UPSTREAM_CA is
set, concatenate /etc/ssl/certs/ca-certificates.crt + the pipelock
CA into /home/mitmproxy/.mitmproxy/combined-trust.pem, and pass
that as ssl_verify_upstream_trusted_ca. Covers both legs:

  - pipelock-MITM'd hosts → leaf cert signed by pipelock CA →
    validates against the pipelock half of the bundle.
  - pipelock-passthrough hosts (api.anthropic.com et al.) → real
    upstream cert → validates against the system half.

Standalone runs of the image (no EGRESS_PROXY_UPSTREAM_CA) skip
the concat and use mitmproxy's default trust store.

Reproduces against today's main: agent gets "Unable to connect to
API: SSL certificate verification failed" on api.anthropic.com,
egress-proxy logs "Server TLS handshake failed. Certificate verify
failed: unable to get local issuer certificate". After this patch
the trust bundle includes the real upstream root + pipelock's CA
and both validation paths succeed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:52:08 -04:00
didericis 70f773ac61 feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s
Hard cutover. cred-proxy is deleted; egress-proxy is now the agent's
HTTP_PROXY (when routes are declared) with pipelock on its outbound
leg. Two per-bottle CAs are minted: egress-proxy's (agent trust
store) and pipelock's (egress-proxy's outbound trust store).

Manifest:
  - `bottle.cred_proxy` → hard error with a migration recipe.
  - `bottle.egress_proxy` is the new shape (PRD 0017 chunk 1).
  - CredProxy* types + role validators removed.

Wiring:
  - launch.py: `egress_proxy_tls_init` mints the egress-proxy CA
    (cert+key concat for mitmproxy + cert-only for agent trust);
    `DockerEgressProxy.start` docker-cps both CAs in, sets
    `HTTPS_PROXY=pipelock` + `EGRESS_PROXY_UPSTREAM_CA` so mitmdump
    trusts pipelock's MITM. Agent's HTTP_PROXY points at
    egress-proxy when routes exist, else falls back to pipelock
    (no-routes bottles unchanged).
  - prepare.py / backend.py: `cred_proxy` arg → `egress_proxy`;
    sidecar-orphan probe + plan field + dashboard view all
    renamed.
  - provision_ca: selects the egress-proxy CA when present, else
    pipelock's (filename renamed to claude-bottle-mitm-ca.crt).
  - bottle.provision: cred-proxy dotfile rewrites (~/.npmrc,
    ~/.gitconfig insteadOf, tea config) are gone — HTTP_PROXY
    catches everything respecting it.

Pipelock helpers:
  - `pipelock_token_hosts` → `pipelock_route_hosts` (now reading
    egress_proxy.routes).
  - cred-proxy hostname auto-allow → egress-proxy hostname
    auto-allow.
  - Anthropic seed-phrase workaround now triggers when an
    egress_proxy route targets api.anthropic.com (was based on the
    cred-proxy `anthropic-base-url` role).

Dockerfile.egress-proxy:
  - Entrypoint conditionally passes
    `--set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA`
    (via the `${VAR:+...}` shell expansion) so standalone runs without
    a mounted pipelock CA still boot.
  - mkdirs `/home/mitmproxy/.mitmproxy` ahead of `docker cp`.

Deleted: claude_bottle/{cred_proxy,cred_proxy_server}.py,
backend/docker/{cred_proxy,provision/cred_proxy}.py,
Dockerfile.cred-proxy, plus the corresponding unit + integration
tests. backend/docker/cred_proxy_apply.py stays as a stub for
chunk 3 to rewrite (its container-name + routes-path constants
are inlined so it survives without the deleted module).

Test changes:
  - test_pipelock_allowlist rewritten against egress-proxy routes
    + the new `pipelock_route_hosts`.
  - test_manifest_md_load + test_pipelock_yaml + test_yaml_subset
    fixtures migrated to the `egress_proxy: { routes: [...] }`
    shape.
  - test_supervise_sidecar's round-trip test switched from
    `dashboard.approve` to `dashboard.reject`: the approval-apply
    path on cred-proxy-block proposals hits a deleted sidecar in
    chunk 2's transitional state. Chunk 3 restores the approval
    test once the remediation flow is retargeted at egress-proxy.

376 tests pass (was 427; net delta is removed cred-proxy tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:30:39 -04:00
didericis 3df54573d4 feat(egress-proxy): add mitmproxy-based sidecar core (PRD 0017 chunk 1)
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m39s
Lands the new egress-proxy artifact alongside cred-proxy. Chunk 2
wires the agent's HTTP_PROXY to it and removes cred-proxy.

  - `Dockerfile.egress-proxy` — mitmproxy 11.1.3 base, COPY addon
    files flat to /app, mkdir routes dir at /etc/egress-proxy/.
    Digest pin deferred to chunk 2.
  - `egress_proxy_addon_core.py` — pure-logic parse + decide
    (host-importable; 21 unit tests).
  - `egress_proxy_addon.py` — mitmproxy hook wrapper, container-only
    (boot + SIGHUP reload, strip-Authorization + decide + 403/inject).
  - `egress_proxy.py` — host helpers: manifest lift, routes.yaml
    render (JSON content), token-env-map, Plan + abstract class.
  - `backend/docker/egress_proxy.py` — `DockerEgressProxy` start/stop
    mirroring `DockerCredProxy`; not yet called from launch.py.
  - `manifest.py` — new `EgressProxyRoute` + `EgressProxyConfig` types
    with the nested `auth: { scheme, token_ref }` block per PRD;
    `bottle.egress_proxy` added to the bottle key set alongside
    `cred_proxy` (chunk 2 hard-fails on the latter).

All 427 unit tests pass. Image builds; `docker run` boots mitmdump
and the addon loads routes from a mounted routes.yaml.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:58:24 -04:00