refactor(pipelock): drop bottle.ssh carve-outs

PRD 0007: SSH traffic now flows through the per-agent ssh-gate
sidecar, so pipelock should know nothing about bottle.ssh.

Removed:
- pipelock_bottle_ssh_hostnames, _trusted_domains, _ip_cidrs.
- The trusted_domains / ssrf blocks built from ssh entries.
- pipelock_proxy_host_port — its last caller (the ssh provisioner)
  is gone.
- is_ipv4_literal — only used to classify ssh hostnames into
  trusted_domains vs ssrf.ip_allowlist, both of which are gone.

api_allowlist now derives solely from baked-in defaults +
bottle.egress.allowlist. Tests updated to pin the new shape and
assert ssh hostnames do NOT leak into pipelock's config.
This commit is contained in:
2026-05-12 16:08:26 -04:00
parent ce948db0b7
commit 6130ea385f
6 changed files with 29 additions and 122 deletions
+10 -24
View File
@@ -1,20 +1,12 @@
"""Unit: pipelock_effective_allowlist — the union of baked-in defaults,
bottle.egress.allowlist, and bottle.ssh[].Hostname. Plus a small check
that IPv4 hostnames pick up the /32 suffix when classified as CIDRs.
The lower-level one-line helpers (pipelock_bottle_allowlist,
pipelock_bottle_ssh_hostnames, pipelock_bottle_ssh_trusted_domains)
are exercised end-to-end by test_union_and_dedup, so they don't get
their own tests."""
"""Unit: pipelock_effective_allowlist — the union of baked-in defaults
and bottle.egress.allowlist. Per PRD 0007, bottle.ssh entries do NOT
contribute (SSH traffic goes through the per-agent ssh-gate, not
pipelock)."""
import unittest
from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import (
pipelock_bottle_ssh_ip_cidrs,
pipelock_effective_allowlist,
)
from tests.fixtures import fixture_with_ssh
from claude_bottle.pipelock import pipelock_effective_allowlist
class TestEffectiveAllowlist(unittest.TestCase):
@@ -36,20 +28,14 @@ class TestEffectiveAllowlist(unittest.TestCase):
eff = pipelock_effective_allowlist(manifest.bottles["dev"])
self.assertIn("api.anthropic.com", eff, "baked default present")
self.assertIn("registry.npmjs.org", eff, "egress.allowlist present")
self.assertIn("100.78.141.42", eff, "ssh ipv4 hostname present")
self.assertIn("github.com", eff, "ssh hostname present")
# PRD 0007: ssh hostnames must not contribute to pipelock's
# allowlist anymore — they're routed through the ssh-gate
# sidecar, which is on its own egress path.
self.assertNotIn("100.78.141.42", eff)
self.assertNotIn("github.com", eff)
self.assertEqual(len(eff), len(set(eff)), "deduplicated")
self.assertEqual(eff, sorted(eff), "sorted")
class TestSSHIPCidrs(unittest.TestCase):
def test_ipv4_hostname_gets_32_suffix(self):
cidrs = pipelock_bottle_ssh_ip_cidrs(fixture_with_ssh().bottles["dev"])
self.assertIn("100.78.141.42/32", cidrs)
# Hostname-typed entries don't end up here.
self.assertNotIn("github.com", cidrs)
self.assertNotIn("github.com/32", cidrs)
if __name__ == "__main__":
unittest.main()
-33
View File
@@ -1,33 +0,0 @@
"""Unit: is_ipv4_literal — the classifier that decides whether
bottle.ssh[].Hostname goes into pipelock's ssrf.ip_allowlist (IPv4
literal) or trusted_domains (hostname)."""
import unittest
from claude_bottle.util import is_ipv4_literal
class TestIPv4Classify(unittest.TestCase):
def test_positive(self):
for ip in ("127.0.0.1", "10.0.0.5", "100.78.141.42", "0.0.0.0", "255.255.255.255"):
with self.subTest(ip=ip):
self.assertTrue(is_ipv4_literal(ip), ip)
def test_negative(self):
for hn in (
"github.com",
"gitea.dideric.is",
"100.78.141",
"100.78.141.42.5",
"::1",
"fe80::1",
"localhost",
"",
"1.2.3.4.example.com",
):
with self.subTest(hn=hn):
self.assertFalse(is_ipv4_literal(hn), hn)
if __name__ == "__main__":
unittest.main()
+15 -12
View File
@@ -34,23 +34,25 @@ class TestBuildConfig(unittest.TestCase):
# Baked defaults always present.
self.assertIn("api.anthropic.com", cast(list[str], cfg["api_allowlist"]))
self.assertIn("raw.githubusercontent.com", cast(list[str], cfg["api_allowlist"]))
# No SSH entries → no trusted_domains, no ssrf.
# PRD 0007: pipelock has no SSH carve-outs at all — neither
# trusted_domains nor ssrf are ever emitted from bottle data
# in v1.
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):
def test_ssh_entries_do_not_leak_into_pipelock(self):
# PRD 0007: bottle.ssh routes through the ssh-gate sidecar,
# so pipelock's config must not reflect those hostnames or
# IPs in any of its blocks.
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
self.assertIn("github.com", cast(list[str], cfg["trusted_domains"]))
self.assertNotIn("100.78.141.42", cast(list[str], cfg["trusted_domains"]))
self.assertIn(
"100.78.141.42/32",
cast(dict[str, Any], cfg["ssrf"])["ip_allowlist"],
)
# Strict mode: IPv4 host is also in the api_allowlist union.
self.assertIn("100.78.141.42", cast(list[str], cfg["api_allowlist"]))
allow = cast(list[str], cfg["api_allowlist"])
self.assertNotIn("github.com", allow)
self.assertNotIn("100.78.141.42", allow)
self.assertNotIn("trusted_domains", cfg)
self.assertNotIn("ssrf", cfg)
def test_tls_interception_block_emitted_when_paths_supplied(self):
# PRD 0006: paths flow in via DockerPipelockProxy's in-container
@@ -95,12 +97,13 @@ class TestRenderAndWrite(unittest.TestCase):
for required in (
"api_allowlist:",
"forward_proxy:",
"trusted_domains:",
"ssrf:",
"dlp:",
"request_body_scanning:",
):
self.assertIn(required, text)
# PRD 0007: no ssh carve-outs in the rendered yaml.
self.assertNotIn("trusted_domains:", text)
self.assertNotIn("ssrf:", text)
def test_prepare_writes_file_at_mode_600(self):
plan = DockerPipelockProxy().prepare(