From c5d729e25dee3c39a93cb6d82949192567f40f21 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 13:49:31 -0400 Subject: [PATCH] fix(pipelock): suppress BIP-39 detector on cred-proxy anthropic path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude-code's chat bodies legitimately trip pipelock's BIP-39 seed- phrase detector — any 12+ English words that pass the BIP-39 checksum match. The direct path to api.anthropic.com already sits on tls_interception.passthrough_domains so no body scan runs there, but the cred-proxy hop is plain HTTP through pipelock and the body scanner fires. Add an anthropic-route-specific suppress entry: suppress: - rule: "BIP-39 Seed Phrase" path: "/anthropic/**" Just this one detector, only on this one path. Every other DLP pattern (AKIA, gh*_, sk-ant-, etc.) keeps firing — those are unambiguous credential shapes with no legitimate reason to appear in a chat completion. Other detectors that fire on natural language can be added to the suppress list when/if they surface. Wiring: pipelock_effective_suppress(bottle) computes the entries from bottle.cred_proxy.routes; pipelock_build_config accepts them and emits a `suppress:` block; pipelock_render_yaml renders it. Probed schema with `pipelock check --config` to confirm the {rule, path} shape; full yaml validates clean. --- claude_bottle/pipelock.py | 34 ++++++++++++++++++++++++++ tests/unit/test_pipelock_yaml.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/claude_bottle/pipelock.py b/claude_bottle/pipelock.py index c4f30cf..7721968 100644 --- a/claude_bottle/pipelock.py +++ b/claude_bottle/pipelock.py @@ -99,6 +99,31 @@ def pipelock_effective_allowlist(bottle: Bottle) -> list[str]: return sorted(seen.keys()) +def pipelock_effective_suppress(bottle: Bottle) -> list[dict[str, str]]: + """Per-bottle pipelock detector suppressions. + + Pipelock's `suppress:` block silences a named rule on a path glob. + LLM conversation bodies legitimately trip detectors that look for + natural-language token shapes — most famously the BIP-39 seed- + phrase detector, which fires on any 12+ English words that pass + the BIP-39 checksum. The direct path to `api.anthropic.com` is + already on tls_interception.passthrough_domains so no body scan + runs there, but the cred-proxy hop (where the agent dials + `http://cred-proxy:9099/anthropic/...`) is plain HTTP through + pipelock — body scanning fires. + + For each route with the `anthropic-base-url` role, suppress + BIP-39 on `**` so claude-code's chat bodies make it + through. All other detectors (credit-card, IBAN, token regexes, + etc.) keep firing — those are unambiguous credential shapes + that have no legitimate reason to appear in a chat completion.""" + out: list[dict[str, str]] = [] + for r in bottle.cred_proxy.routes: + if "anthropic-base-url" in r.Role: + out.append({"rule": "BIP-39 Seed Phrase", "path": f"{r.Path}**"}) + return out + + def pipelock_effective_tls_passthrough(bottle: Bottle) -> list[str]: """Hostnames pipelock should pass through (no TLS MITM, no body scan). Default carries the LLM API endpoint — its request bodies @@ -185,6 +210,9 @@ def pipelock_build_config( "api_allowlist": pipelock_effective_allowlist(bottle), "forward_proxy": {"enabled": True}, } + suppress = pipelock_effective_suppress(bottle) + if suppress: + cfg["suppress"] = suppress cfg["dlp"] = {"include_defaults": True, "scan_env": True} # Body-scan enforcement is a separate pipelock section (each DLP # "surface" — body, MCP, response — has its own action). Pipelock's @@ -225,6 +253,12 @@ def pipelock_render_yaml(cfg: dict[str, object]) -> str: for h in cast(list[str], cfg["api_allowlist"]): lines.append(f' - "{h}"') lines.append("") + if "suppress" in cfg: + lines.append("suppress:") + for entry in cast(list[dict[str, str]], cfg["suppress"]): + lines.append(f' - rule: "{entry["rule"]}"') + lines.append(f' path: "{entry["path"]}"') + lines.append("") lines.append("forward_proxy:") fp = cast(dict[str, object], cfg["forward_proxy"]) lines.append(f" enabled: {_bool(fp['enabled'])}") diff --git a/tests/unit/test_pipelock_yaml.py b/tests/unit/test_pipelock_yaml.py index ac3bb36..f6fefd8 100644 --- a/tests/unit/test_pipelock_yaml.py +++ b/tests/unit/test_pipelock_yaml.py @@ -92,6 +92,31 @@ class TestBuildConfig(unittest.TestCase): self.assertIn("ssrf", cfg) self.assertEqual({"ip_allowlist": ["172.20.0.0/16"]}, cfg["ssrf"]) + def test_suppress_absent_when_no_anthropic_route(self): + cfg = pipelock_build_config(fixture_minimal().bottles["dev"]) + self.assertNotIn("suppress", cfg) + + def test_suppress_emits_bip39_for_anthropic_route(self): + # claude-code's chat bodies trip pipelock's BIP-39 detector + # (12+ English words). Suppress just that detector on the + # cred-proxy's anthropic path — all the other DLP patterns + # keep firing. + from claude_bottle.manifest import Manifest + bottle = Manifest.from_json_obj({ + "bottles": {"dev": {"cred_proxy": {"routes": [ + {"path": "/anthropic/", + "upstream": "https://api.anthropic.com", + "auth_scheme": "Bearer", "token_ref": "T", + "role": "anthropic-base-url"}, + ]}}}, + "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, + }).bottles["dev"] + cfg = pipelock_build_config(bottle) + self.assertEqual( + [{"rule": "BIP-39 Seed Phrase", "path": "/anthropic/**"}], + cfg["suppress"], + ) + class TestRenderAndWrite(unittest.TestCase): def setUp(self): @@ -175,6 +200,22 @@ class TestRenderAndWrite(unittest.TestCase): self.assertIn("ip_allowlist:", text) self.assertIn('- "172.20.0.0/16"', text) + def test_render_emits_suppress_block_for_anthropic_route(self): + from claude_bottle.manifest import Manifest + bottle = Manifest.from_json_obj({ + "bottles": {"dev": {"cred_proxy": {"routes": [ + {"path": "/anthropic/", + "upstream": "https://api.anthropic.com", + "auth_scheme": "Bearer", "token_ref": "T", + "role": "anthropic-base-url"}, + ]}}}, + "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, + }).bottles["dev"] + text = pipelock_render_yaml(pipelock_build_config(bottle)) + self.assertIn("suppress:", text) + self.assertIn('rule: "BIP-39 Seed Phrase"', text) + self.assertIn('path: "/anthropic/**"', text) + if __name__ == "__main__": unittest.main()