feat(pipelock): enable tls_interception with per-bottle ephemeral CA
First step of PRD 0006. Pipelock now does the CONNECT bumping that PR #8's mitmproxy chain was supposed to provide — natively, in the same single sidecar PRD 0001 wired up. - claude_bottle/pipelock.py: pipelock_build_config grows optional ca_cert_path / ca_key_path kwargs. When both are passed the rendered YAML carries a `tls_interception: { enabled: true, ca_cert, ca_key }` block. PipelockProxy gains class-level CA_CERT_IN_CONTAINER / CA_KEY_IN_CONTAINER constants that subclasses set to wherever they place the CA inside the sidecar. PipelockProxyPlan gains ca_cert_host_path / ca_key_host_path fields (default empty Path() — sentinel for "not yet populated", filled by launch via dataclasses.replace). - claude_bottle/backend/docker/pipelock.py: new pipelock_tls_init(stage_dir) helper runs `pipelock tls init` in a one-shot container against a host-mounted scratch dir. DockerPipelockProxy sets its class constants to /etc/pipelock-ca.pem and /etc/pipelock-ca-key.pem; .start docker-cp's the cert + key into those paths between `docker create` and `docker start`. Pipelock runs as root in its distroless image, so no chown is needed (verified). - claude_bottle/backend/docker/launch.py: calls pipelock_tls_init between network creation and proxy.start. Prepare stays side-effect-free on docker; the one-shot ca-init container only runs on a real launch, not on `start --dry-run`. - tests/unit/test_pipelock_yaml.py: new assertions that pipelock_build_config emits the tls_interception block only when both paths are supplied (and rejects a half-set pair), plus a test that the docker proxy's prepare plumbs the in-container paths through to the rendered YAML. The end-to-end "bumping actually fires" assertion lands in chunk 4 (HTTPS integration tests). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,9 @@ class TestBuildConfig(unittest.TestCase):
|
||||
# No SSH entries → no trusted_domains, no ssrf.
|
||||
self.assertNotIn("trusted_domains", cfg)
|
||||
self.assertNotIn("ssrf", cfg)
|
||||
# Without CA paths, the tls_interception block is omitted —
|
||||
# pipelock falls back to its built-in default of `enabled: false`.
|
||||
self.assertNotIn("tls_interception", cfg)
|
||||
|
||||
def test_ssh_shape(self):
|
||||
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
|
||||
@@ -49,6 +52,31 @@ class TestBuildConfig(unittest.TestCase):
|
||||
# Strict mode: IPv4 host is also in the api_allowlist union.
|
||||
self.assertIn("100.78.141.42", cast(list[str], cfg["api_allowlist"]))
|
||||
|
||||
def test_tls_interception_block_emitted_when_paths_supplied(self):
|
||||
# PRD 0006: paths flow in via DockerPipelockProxy's in-container
|
||||
# constants; this directly pins the dict shape.
|
||||
cfg = pipelock_build_config(
|
||||
fixture_minimal().bottles["dev"],
|
||||
ca_cert_path="/etc/pipelock-ca.pem",
|
||||
ca_key_path="/etc/pipelock-ca-key.pem",
|
||||
)
|
||||
self.assertEqual(
|
||||
{
|
||||
"enabled": True,
|
||||
"ca_cert": "/etc/pipelock-ca.pem",
|
||||
"ca_key": "/etc/pipelock-ca-key.pem",
|
||||
},
|
||||
cfg["tls_interception"],
|
||||
)
|
||||
|
||||
def test_tls_interception_requires_both_paths(self):
|
||||
# Half-set is a programmer error, not a silent omission.
|
||||
with self.assertRaises(ValueError):
|
||||
pipelock_build_config(
|
||||
fixture_minimal().bottles["dev"],
|
||||
ca_cert_path="/etc/pipelock-ca.pem",
|
||||
)
|
||||
|
||||
|
||||
class TestRenderAndWrite(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -101,6 +129,21 @@ class TestRenderAndWrite(unittest.TestCase):
|
||||
self.assertNotIn("MY_SECRET", content)
|
||||
self.assertNotIn("prompt-message", content)
|
||||
|
||||
def test_render_emits_tls_interception_via_prepare(self):
|
||||
"""`DockerPipelockProxy.prepare` plumbs its in-container CA
|
||||
constants through to the YAML. The block should land in the
|
||||
rendered output with `enabled: true` and the configured paths.
|
||||
The actual host-side CA generation happens in launch (not
|
||||
prepare), so this test exercises only the YAML rendering."""
|
||||
plan = DockerPipelockProxy().prepare(
|
||||
fixture_minimal().bottles["dev"], "demo", self.out_dir
|
||||
)
|
||||
content = plan.yaml_path.read_text()
|
||||
self.assertIn("tls_interception:", content)
|
||||
self.assertIn("enabled: true", content)
|
||||
self.assertIn('ca_cert: "/etc/pipelock-ca.pem"', content)
|
||||
self.assertIn('ca_key: "/etc/pipelock-ca-key.pem"', content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user