fix(pipelock): suppress BIP-39 detector on cred-proxy anthropic path
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.
This commit is contained in:
@@ -99,6 +99,31 @@ def pipelock_effective_allowlist(bottle: Bottle) -> list[str]:
|
|||||||
return sorted(seen.keys())
|
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 `<path>**` 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]:
|
def pipelock_effective_tls_passthrough(bottle: Bottle) -> list[str]:
|
||||||
"""Hostnames pipelock should pass through (no TLS MITM, no body
|
"""Hostnames pipelock should pass through (no TLS MITM, no body
|
||||||
scan). Default carries the LLM API endpoint — its request bodies
|
scan). Default carries the LLM API endpoint — its request bodies
|
||||||
@@ -185,6 +210,9 @@ def pipelock_build_config(
|
|||||||
"api_allowlist": pipelock_effective_allowlist(bottle),
|
"api_allowlist": pipelock_effective_allowlist(bottle),
|
||||||
"forward_proxy": {"enabled": True},
|
"forward_proxy": {"enabled": True},
|
||||||
}
|
}
|
||||||
|
suppress = pipelock_effective_suppress(bottle)
|
||||||
|
if suppress:
|
||||||
|
cfg["suppress"] = suppress
|
||||||
cfg["dlp"] = {"include_defaults": True, "scan_env": True}
|
cfg["dlp"] = {"include_defaults": True, "scan_env": True}
|
||||||
# Body-scan enforcement is a separate pipelock section (each DLP
|
# Body-scan enforcement is a separate pipelock section (each DLP
|
||||||
# "surface" — body, MCP, response — has its own action). Pipelock's
|
# "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"]):
|
for h in cast(list[str], cfg["api_allowlist"]):
|
||||||
lines.append(f' - "{h}"')
|
lines.append(f' - "{h}"')
|
||||||
lines.append("")
|
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:")
|
lines.append("forward_proxy:")
|
||||||
fp = cast(dict[str, object], cfg["forward_proxy"])
|
fp = cast(dict[str, object], cfg["forward_proxy"])
|
||||||
lines.append(f" enabled: {_bool(fp['enabled'])}")
|
lines.append(f" enabled: {_bool(fp['enabled'])}")
|
||||||
|
|||||||
@@ -92,6 +92,31 @@ class TestBuildConfig(unittest.TestCase):
|
|||||||
self.assertIn("ssrf", cfg)
|
self.assertIn("ssrf", cfg)
|
||||||
self.assertEqual({"ip_allowlist": ["172.20.0.0/16"]}, cfg["ssrf"])
|
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):
|
class TestRenderAndWrite(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -175,6 +200,22 @@ class TestRenderAndWrite(unittest.TestCase):
|
|||||||
self.assertIn("ip_allowlist:", text)
|
self.assertIn("ip_allowlist:", text)
|
||||||
self.assertIn('- "172.20.0.0/16"', 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user