From f44e884d8afaa832366e97173b439f44be08c612 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 12 May 2026 14:22:59 -0400 Subject: [PATCH] docs(prd): fold 0006 walkthrough resolutions into the design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the open-question walkthrough, all four collapsed: - Q1 (mount semantics): resolved to `docker cp` between `docker create` and `docker start`, mirroring the existing pipelock YAML handling. No bind mount, no UID/permission concern. Folded into §Proposed Design > CA lifecycle as "Sidecar install". - Q2 (cert validity / TTL): pre-decided in the question text. Per-bottle ephemerality is enforced by regenerating per launch, not by short validity windows. Pipelock's defaults are fine. Folded into §Proposed Design as a one-line "Per-bottle ephemerality" note. - Q3 (`passthrough_domains` shape): not v1 scope; the shape is pre-recorded so the follow-up is mechanical. Moved into §Out of scope. - Q4 (stage-dir cleanup ordering): reading start.py confirmed the ExitStack-then-outer-finally order is correct. Folded into §Proposed Design as a "Teardown" note. The §Open questions section is dropped. None of the four was a real design question — they were verifications and pre-decided items left in for defensiveness. Co-Authored-By: Claude Opus 4.7 --- docs/prds/0006-pipelock-tls-interception.md | 72 +++++++++------------ 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/docs/prds/0006-pipelock-tls-interception.md b/docs/prds/0006-pipelock-tls-interception.md index 20bbe7b..d416252 100644 --- a/docs/prds/0006-pipelock-tls-interception.md +++ b/docs/prds/0006-pipelock-tls-interception.md @@ -136,9 +136,13 @@ The feature is **done** when all of the following ship: leaving `ca.pem` and `ca-key.pem` under `stage_dir`. The host file owner is whatever the upstream image's user is; the sidecar mount is read-only so this is fine. - - `DockerPipelockProxy.start` mounts the stage dir into the - sidecar at `/h:ro` and references the CA paths in the rendered - YAML. + - `DockerPipelockProxy.start` `docker cp`s the CA cert + key + into the sidecar at `/etc/pipelock/ca.pem` and + `/etc/pipelock/ca-key.pem` between `docker create` and + `docker start`, mirroring the existing pattern for the YAML + config. If pipelock's image runs as non-root, a `docker exec + -u 0 chown pipelock:pipelock /etc/pipelock/ca*.pem` lands + between the `cp` and the `start`. - **`claude_bottle/backend/__init__.py`**: new abstract method `provision_ca(plan, target)` on `BottleBackend`, default no-op. `BottleBackend.provision` orchestrates `ca → prompt → skills → @@ -183,7 +187,10 @@ The feature is **done** when all of the following ship: - A manifest field to disable / customize interception per bottle. Doable but premature. - Wiring `passthrough_domains`. The default `[]` is correct for - v1; add the manifest field when a pinning host shows up. + v1; add the manifest field when a pinning host shows up. The + shape is pre-recorded so the follow-up is mechanical: + `bottle.egress.tls_passthrough_domains: [host, ...]`, + mirroring the existing `egress.allowlist`. - `cross_request_detection`, `entropy_budget`, `fragment_reassembly`, `reverse_proxy`, `scan_api` — features pipelock exposes but we don't need for the body-DLP gap. @@ -204,21 +211,29 @@ generated at prepare time. ### CA lifecycle - **Generation.** Host-side, at prepare time, via a one-shot - `docker run --rm -v :/h pipelock tls init`. Output is - `/ca.pem` + `/ca-key.pem`, both mode 600. -- **Sidecar mount.** `DockerPipelockProxy.start` adds - `-v :/h:ro` to the sidecar's `docker run`. The rendered - YAML references `/h/ca.pem` and `/h/ca-key.pem`. The private - key is read-only from pipelock's perspective; the host stage - dir is owned by the launching user. + `docker run --rm -v :/h -e PIPELOCK_HOME=/h pipelock tls + init`. Output: `/ca.pem` + `/ca-key.pem`, mode 600. +- **Sidecar install.** `DockerPipelockProxy.start` `docker cp`s + the CA cert + key into the sidecar at `/etc/pipelock/ca.pem` + and `/etc/pipelock/ca-key.pem` between `docker create` and + `docker start`. Same pattern the proxy already uses for the + YAML config — no bind-mount, no UID/permission concern from + the one-shot generation step. The rendered YAML references + the in-container paths. - **Bottle install.** `provision_ca` (Docker impl) does `docker cp /ca.pem agent:/usr/local/share/ca-certificates/claude-bottle-mitm.crt`, then `update-ca-certificates`. The CA env trio is set at `docker run -e` time (Docker propagates run-time env into - `docker exec`, verified in PR #8's spike). -- **Teardown.** The sidecar container is destroyed, the stage - dir is removed by `start.py`'s existing `finally` block, and - the CA dies with both. + `docker exec`). +- **Per-bottle ephemerality.** Enforced by *regenerating per + launch*, not by validity windows. Pipelock's defaults + (`cert_ttl: 24h` for leaves, `--validity 87600h` for the CA) + are fine — the CA lives only as long as the sidecar, which is + the bottle's lifetime. +- **Teardown.** Sidecar removed via `ExitStack` callback, then + the launch context manager's outer `finally` `shutil.rmtree`s + `stage_dir`. CA dies with both, in that order, so the sidecar + is never reading a deleted mount on shutdown. - **Fingerprint.** Computed via stdlib in `provision_ca` and logged once to stderr (`claude-bottle: mitm ca fingerprint: sha256:…`). The private key never appears in any log. @@ -259,33 +274,6 @@ backend module. image's own `tls init` command in a one-shot container. Fingerprint uses Python stdlib `ssl` + `hashlib`. -## Open questions - -- **Mount semantics for the stage dir.** The sidecar runs with a - `-v :/h:ro` bind mount. The CA files were written by - the one-shot `pipelock tls init` container with whatever UID - pipelock's image uses; the sidecar reads them as that same UID. - Should work, but confirm on first impl by inspecting the file - modes/owners and that the sidecar actually loads them. Fallback: - `docker cp` the cert/key into the running sidecar after `docker - create` (mirror PR #8's mitmproxy lifecycle). -- **Cert validity / TTL.** Defaults are `cert_ttl: 24h` for - per-host leaves; the CA validity from `pipelock tls init` is - 10 years by default (`--validity 87600h`). The CA outlives the - bottle either way; per-bottle ephemerality is enforced by - *generating a fresh one each launch*, not by setting a short - CA validity. Document; no tuning in v1. -- **`passthrough_domains` shape.** Once we expose this through - the manifest in a follow-up, the natural place is - `bottle.egress.tls_passthrough_domains: [host, ...]`, mirroring - the existing `egress.allowlist` shape. -- **Stage-dir cleanup ordering.** The stage dir holds the CA - private key briefly. `start.py`'s existing `finally` block - `shutil.rmtree`s it. Confirm the rmtree fires after the sidecar - is stopped, so the sidecar isn't reading a deleted mount when - it shuts down. The current order is correct (teardown unwinds - via ExitStack before the outer `finally` runs); verify. - ## References - `docs/research/pipelock-assessment.md` (now corrected) —