security(mitmproxy): split volume so agent cannot read CA private key - #97
security(mitmproxy): split volume so agent cannot read CA private key#97shejnowicz wants to merge 8 commits into
Conversation
Two-volume topology closes the CA private-key exposure to the agent container. mitmproxy-config narrows to private (mitmproxy + wg-client, holds CA priv key + WireGuard priv keys + dns.conf + extra_hosts); new mitmproxy-public holds only agent-facing files (public CA cert, sandcat.env, cursor-cli-config.json). Agent's container-side path stays /mitmproxy-config/ — app-init.sh unchanged. Addon writes agent-visible dynamic files directly to public volume via updated constants; adds _seed_public_ca_cert() to copy the CA cert after mitmproxy generates it. Healthcheck gates on the public cert so the agent depends chain guarantees the file exists before start. Plan: 2 tasks — all interlocking compose+addon+test changes together (they must land as one to keep the stack functional), plus hands-on integration that verifies the agent can no longer read the private key and everything else still works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g() (#25) Two runtime bugs found during Task 2 integration: 1. /mitmproxy-public is root:root on fresh volume mount; chown it in the entrypoint wrapper (runs as root before docker-entrypoint.sh drops privs). 2. CA cert not generated until after load(); move _seed_public_ca_cert() call to running() which fires after TLS engine startup; bump retry to 30s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#25) The running() addon lifecycle hook was not reliably fired — mitmweb routes addon logs to the web UI termlog, silencing the hook. Switch to a background sh loop in the entrypoint that polls for the cert file and copies it once mitmproxy generates it, removing the dependency on Python addon hooks entirely. Remove running() + _seed_public_ca_cert() methods and their now-unused imports (shutil, time) from mitmproxy_addon_common.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plit assert_common_volumes was still checking .source == "mitmproxy-public" agent-home, mitmproxy-public added to assert_named_volumes for both claude and cursor compose helpers. Fixes 2 failing regression tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace cp src dst; chmod 644 dst with cp src dst.tmp && mv dst.tmp dst. mv on the same filesystem is rename(2) which is atomic — the healthcheck predicate 'test -f dst' can no longer observe a partially-written cert. Drops the now-redundant chmod 644 (cp preserves the source mode 644). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves the devcontainer proxy stack’s secret isolation by splitting the previous single mitmproxy-config volume into a private volume (not mounted into the agent) and a new agent-facing mitmproxy-public volume that only contains public artifacts like the CA certificate and generated config sidecars. This addresses issue #25 by preventing the untrusted agent container from reading CA/WireGuard private keys.
Changes:
- Update compose templates so the agent mounts
mitmproxy-publicat/mitmproxy-configwhilemitmproxy-configremains private tomitmproxy+wg-client. - Update mitmproxy addon paths so
sandcat.envandcursor-cli-config.jsonare written into/mitmproxy-public/. - Extend/init regression tests to assert the new volume exists, is mounted correctly, and the old private mount is absent from the agent.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| cli/test/init/regression.bats | Updates regression assertions to expect mitmproxy-public on the agent and declare the new named volume. |
| cli/test/init/extensions.bats | Adds tests asserting mitmproxy-public volume declaration, mount, and healthcheck gating behavior. |
| cli/templates/devcontainer/sandcat/scripts/mitmproxy_addon_common.py | Redirects agent-consumed sidecar outputs to /mitmproxy-public instead of the private mitmproxy home volume. |
| cli/templates/devcontainer/sandcat/compose-proxy.yml | Adds mitmproxy-public volume, copies public CA cert into it, and updates healthcheck to gate on the public cert. |
| cli/templates/devcontainer/compose-all.yml | Switches the agent’s mount from mitmproxy-config to mitmproxy-public while keeping the container path /mitmproxy-config. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # the command) so docker-entrypoint.sh still sees `mitmweb` as $1 and drops | ||
| # privileges to the mitmproxy user. | ||
| entrypoint: ["/bin/sh", "-c", "rm -f /home/mitmproxy/.mitmproxy/dns.conf && exec docker-entrypoint.sh \"$@\"", "sh"] | ||
| entrypoint: ["/bin/sh", "-c", "chown -R mitmproxy:mitmproxy /mitmproxy-public && rm -f /home/mitmproxy/.mitmproxy/dns.conf && (while [ ! -f /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem ]; do sleep 1; done; cp /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem /mitmproxy-public/mitmproxy-ca-cert.pem.tmp && mv /mitmproxy-public/mitmproxy-ca-cert.pem.tmp /mitmproxy-public/mitmproxy-ca-cert.pem) & exec docker-entrypoint.sh \"$@\"", "sh"] |
Prepend rm -f /mitmproxy-public/mitmproxy-ca-cert.pem to the entrypoint chain, next to the dns.conf removal. This closes the race where a stale cert from a previous run (when mitmproxy-public persists but mitmproxy-config resets) causes the healthcheck predicate to pass before the background waiter replaces the file with the fresh CA. Without this, agent init installs the old CA into its trust store while mitmproxy serves TLS with the new CA, causing handshake failure. Addresses Copilot review finding on PR #97. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed in |
Fixes #25.
Summary
Before this PR, sandcat mounted a single
mitmproxy-configDocker volume RO to the agent container at/mitmproxy-config/. That volume contained:mitmproxy-ca.pem— CA cert + private keymitmproxy-ca.p12— PKCS12 with private keywireguard.conf— WireGuard private keysBecause uid 1000 in the
mitmproxy/mitmproxy:latestimage aliases tovscode(uid 1000) in the agent, mode 0600 didn't help — the agent could read all of them (confirmed live pre-fix:docker exec -u vscode agent head -1 /mitmproxy-config/mitmproxy-ca.pemreturned-----BEGIN RSA PRIVATE KEY-----).This PR splits the volume:
mitmproxy-config(existing name, scope narrows) — private files. Mounted writable at/home/mitmproxy/.mitmproxy/on mitmproxy, RO at/mitmproxy-config/on wg-client (trusted, needswireguard.conf). Not mounted to agent.mitmproxy-public(new) — agent-facing files only: public CA cert,sandcat.env,cursor-cli-config.json. Mounted writable at/mitmproxy-public/on mitmproxy, RO at/mitmproxy-config/on agent (unchanged container-side path soapp-init.shdoesn't need to change).The mitmproxy addon writes
sandcat.envandcursor-cli-config.jsondirectly to/mitmproxy-public/. The public CA cert is copied by an entrypoint background bash loop that polls until mitmproxy generates it, thencp .tmp && mvatomically to the public volume. The mitmproxy service's healthcheck gates on the public cert being present, so wg-client (depends_on: mitmproxy: healthy) — and by transitivity the agent — start only after the public volume is populated.Test plan
extensions.bats+ regression tests updated inregression.bats)sandcat init --agent claude+ full compose stack:docker volume ls— bothmitmproxy-config(private) andmitmproxy-public(agent-facing) existdocker exec -u vscode agent ls /mitmproxy-config/— only public files:mitmproxy-ca-cert.pem,sandcat.env,cursor-cli-config.jsondocker exec -u vscode agent cat /mitmproxy-config/mitmproxy-ca.pem→No such file or directory(exit 1)docker exec -u vscode agent cat /mitmproxy-config/wireguard.conf→No such file or directory(exit 1)curl https://api.github.com/returnsHTTP/2 200Notes on the fix history (visible in the branch's commit log)
Two runtime fix rounds after the initial split:
f61b621— freshly-mounted Docker volume is root-owned; mitmproxy user (uid 1000) couldn't write. Addedchown -R mitmproxy:mitmproxy /mitmproxy-publicto the mitmproxy entrypoint (runs as root beforedocker-entrypoint.shdrops privileges).9559451— the initial approach called_seed_public_ca_cert()from the addon'srunning()hook. The hook wasn't reliably invoked by mitmweb (no seed log ever appeared even after minutes of uptime). Switched to a background bashwhile [ ! -f ...]; do sleep 1; doneloop in the entrypoint that polls for the cert and copies it once it appears — version-independent of mitmproxy addon lifecycle. Removed the addon-side seed method entirely.Then a final review fix wave (
bb864d9+267729f) updated stale regression tests and made the copy atomic via.tmp+mv.Upgrade path for existing projects
Docker auto-creates the new
mitmproxy-publicvolume on nextsandcat run --build. Existingmitmproxy-configvolumes keep their contents but the agent-side mount changes to the new volume, so the stalesandcat.env/cursor-cli-config.jsonbecome unreachable from the agent (they're re-written to the new volume by the addon). No user action required.🤖 Generated with Claude Code