From 57a9707e1c278c5627a10bcdefcb674a870a0e8f Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 15:42:51 -0400 Subject: [PATCH] fix(egress-proxy): chmod 644 host CAs so mitmproxy user can read after docker cp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mitmdump crashed at boot with PermissionError on ~/.mitmproxy/mitmproxy-ca.pem. Cause: `docker cp` preserves the host file's mode AND uid. The CA files were 0600 owned by the host user (uid 501 on macOS), so inside the container the mitmproxy user (uid 1000, set by USER directive in Dockerfile) couldn't read them. Fix: - `egress_proxy_tls_init`: chmod 644 the cert-only + the cert+key concat on the host stage dir. - `DockerEgressProxy.start`: chmod 644 routes.yaml and the pipelock CA before `docker cp` into the egress-proxy container (pipelock itself runs as root so its in-pipelock copy is unaffected). The host stage_dir is mode 700 — other host users still can't traverse in, so the cert+key concat isn't actually exposed despite the 644 mode. The container side gets world-readable, which is fine inside the per-bottle container. Reproduces against today's main: bottle's egress-proxy sidecar crashes with PermissionError; after this patch mitmdump boots and listens on :9099. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/backend/docker/egress_proxy.py | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/claude_bottle/backend/docker/egress_proxy.py b/claude_bottle/backend/docker/egress_proxy.py index aa965f0..a823179 100644 --- a/claude_bottle/backend/docker/egress_proxy.py +++ b/claude_bottle/backend/docker/egress_proxy.py @@ -115,11 +115,17 @@ def egress_proxy_tls_init(stage_dir: Path) -> tuple[Path, Path]: key = work / "ca-key.pem" if not cert.is_file() or not key.is_file(): die(f"egress-proxy tls init did not produce ca files in {work}") - cert.chmod(0o600) + # Mode 644 (not 600) so `docker cp` preserves world-readability + # inside the container — the mitmproxy user (uid 1000) needs to + # read the file, and the host uid `docker cp` propagates from the + # source doesn't match. The host stage_dir is mode 700 so other + # host users still can't traverse in; the private key isn't + # exposed despite the file mode. + cert.chmod(0o644) # mitmproxy reads cert + key from a single concatenated PEM file. mitm = work / "mitmproxy-ca.pem" mitm.write_bytes(cert.read_bytes() + key.read_bytes()) - mitm.chmod(0o600) + mitm.chmod(0o644) return (mitm, cert) @@ -232,6 +238,17 @@ class DockerEgressProxy(EgressProxy): f"{create_result.stderr.strip()}" ) + # routes.yaml also lands inside the container; bump to 644 + # for the same reason as the CAs — mitmproxy user (uid 1000) + # has to read it. Host stage_dir is mode 700 so the file + # isn't actually exposed to other host users. + plan.routes_path.chmod(0o644) + # Pipelock CA: pipelock itself runs as root so its in-pipelock + # copy doesn't care about mode, but egress-proxy's mitmproxy + # user does. Bump on the host so docker cp into egress-proxy + # carries world-readable. + if route_via_pipelock: + plan.pipelock_ca_host_path.chmod(0o644) cps: list[tuple[Path, str, str]] = [ (plan.routes_path, EGRESS_PROXY_ROUTES_IN_CONTAINER, "routes.yaml"), (plan.mitmproxy_ca_host_path, EGRESS_PROXY_CA_IN_CONTAINER, "mitmproxy CA"),