Files
bot-bottle/tests/unit/test_provision_cred_proxy.py
T
didericis 27b2d78b11
test / unit (pull_request) Successful in 15s
test / integration (pull_request) Successful in 29s
fix(cred_proxy): close git-push bypass + route through pipelock (PRD 0010)
Three coupled fixes that close a documented bypass of git-gate's
gitleaks pre-receive hook:

1. cred-proxy refuses git smart-HTTP push at runtime. Any path
   ending in /git-receive-pack or /info/refs?service=git-receive-pack
   returns 403 with a pointer at the bottle.git SSH path. Fetch
   (upload-pack) is still allowed — the bypass we're closing is
   push, where gitleaks is the load-bearing scanner. Hard guarantee.

2. The provisioner suppresses the cred-proxy `~/.gitconfig` insteadOf
   rewrite for any host already declared in bottle.git. git-gate is
   the canonical git path there; we don't write a competing rule
   that would let `git clone https://<host>/...` succeed in ways
   that confuse on push. Defense in depth — (1) is the hard guarantee.

3. cred-proxy routes its outbound HTTPS through pipelock. The
   sidecar's environ now sets HTTPS_PROXY=<pipelock-url>, and the
   image's entrypoint runs `update-ca-certificates` over the
   per-bottle pipelock CA (docker cp'd into
   /usr/local/share/ca-certificates/pipelock.crt before start) so
   the proxy's HTTPS client trusts pipelock's bumped certs.

   Consequence: pipelock's allowlist + body scanner now sit in the
   cred-proxy egress path the same way they sit in front of direct
   agent traffic. The cred-proxy upstream hosts (api.github.com,
   github.com, gitea hosts, registry.npmjs.org) come OFF
   pipelock's passthrough_domains. Only api.anthropic.com remains
   on passthrough (LLM body content legitimately trips DLP).

PRD 0010 updated to reflect all three. Tests adjusted: the
"cred-proxy hosts go on passthrough" assertion in
test_pipelock_allowlist flips to "they don't", a new
TestIsGitPushRequest exercises the smart-HTTP refusal predicate,
and the gitconfig renderer tests cover the per-host suppression
matrix.
2026-05-13 21:09:33 -04:00

144 lines
5.3 KiB
Python

"""Unit: cred-proxy agent-side provisioner renderers (PRD 0010).
The docker cp / docker exec side effects are exercised by integration
tests; these unit tests cover the pure render functions."""
import unittest
from claude_bottle.backend.docker.provision.cred_proxy import (
render_cred_proxy_gitconfig,
render_npmrc,
render_tea_config,
)
from claude_bottle.cred_proxy import cred_proxy_upstreams_for_bottle
from claude_bottle.manifest import Manifest
def _bottle(tokens):
return Manifest.from_json_obj({
"bottles": {"dev": {"tokens": tokens}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
def _upstreams(tokens):
return cred_proxy_upstreams_for_bottle(_bottle(tokens))
class TestRenderNpmrc(unittest.TestCase):
def test_empty_when_no_npm_route(self):
self.assertEqual("", render_npmrc(_upstreams([])))
self.assertEqual("", render_npmrc(_upstreams([
{"Kind": "anthropic", "TokenRef": "A"},
])))
def test_writes_registry_line(self):
out = render_npmrc(_upstreams([
{"Kind": "npm", "TokenRef": "NPM_TOKEN"},
]))
self.assertEqual("registry=http://cred-proxy:9099/npm/\n", out)
def test_omits_authtoken(self):
# The proxy injects Authorization at request time. The npmrc
# deliberately carries no _authToken — a stale token there
# would just get stripped, but it also creates the false
# impression that the agent holds a credential.
out = render_npmrc(_upstreams([
{"Kind": "npm", "TokenRef": "NPM_TOKEN"},
]))
self.assertNotIn("_authToken", out)
self.assertNotIn("NPM_TOKEN", out)
class TestRenderGitconfig(unittest.TestCase):
def test_empty_when_no_github_or_gitea(self):
self.assertEqual("", render_cred_proxy_gitconfig(_upstreams([
{"Kind": "anthropic", "TokenRef": "A"},
{"Kind": "npm", "TokenRef": "N"},
])))
def test_github_writes_https_insteadof(self):
out = render_cred_proxy_gitconfig(_upstreams([
{"Kind": "github", "TokenRef": "GITHUB_TOKEN"},
]))
self.assertIn('[url "http://cred-proxy:9099/gh-git/"]', out)
self.assertIn("insteadOf = https://github.com/", out)
def test_gitea_writes_per_host_insteadof(self):
out = render_cred_proxy_gitconfig(_upstreams([
{"Kind": "gitea", "TokenRef": "GITEA_TOKEN",
"Url": "https://gitea.dideric.is"},
]))
self.assertIn('[url "http://cred-proxy:9099/gitea/gitea.dideric.is/"]', out)
self.assertIn("insteadOf = https://gitea.dideric.is/", out)
def test_two_giteas_yield_two_rules(self):
out = render_cred_proxy_gitconfig(_upstreams([
{"Kind": "gitea", "TokenRef": "G1",
"Url": "https://gitea.dideric.is"},
{"Kind": "gitea", "TokenRef": "G2",
"Url": "https://gitea.example.com"},
]))
self.assertEqual(2, out.count("insteadOf"))
self.assertIn("gitea.dideric.is/", out)
self.assertIn("gitea.example.com/", out)
def test_github_suppressed_when_git_gate_covers_host(self):
# When bottle.git brokers github.com over SSH, git-gate is the
# canonical git path. The cred-proxy https://github.com/
# rewrite would let the agent push over HTTPS — bypassing
# gitleaks. Suppress it.
out = render_cred_proxy_gitconfig(
_upstreams([{"Kind": "github", "TokenRef": "GH"}]),
{"github.com"},
)
self.assertEqual("", out)
def test_gitea_suppressed_when_git_gate_covers_host(self):
out = render_cred_proxy_gitconfig(
_upstreams([{"Kind": "gitea", "TokenRef": "T",
"Url": "https://gitea.dideric.is"}]),
{"gitea.dideric.is"},
)
self.assertEqual("", out)
def test_partial_suppression_keeps_other_giteas(self):
# Two gitea instances; git-gate brokers one. The other still
# gets the cred-proxy rewrite.
out = render_cred_proxy_gitconfig(
_upstreams([
{"Kind": "gitea", "TokenRef": "T1",
"Url": "https://gitea.dideric.is"},
{"Kind": "gitea", "TokenRef": "T2",
"Url": "https://gitea.example.com"},
]),
{"gitea.dideric.is"},
)
self.assertNotIn("gitea.dideric.is/", out)
self.assertIn("gitea.example.com/", out)
class TestRenderTeaConfig(unittest.TestCase):
def test_empty_when_no_gitea(self):
self.assertEqual("", render_tea_config(_upstreams([
{"Kind": "github", "TokenRef": "G"},
])))
def test_single_gitea_login_block(self):
out = render_tea_config(_upstreams([
{"Kind": "gitea", "TokenRef": "GITEA_TOKEN",
"Url": "https://gitea.dideric.is"},
]))
self.assertIn("logins:", out)
self.assertIn("- name: gitea.dideric.is", out)
self.assertIn("url: http://cred-proxy:9099/gitea/gitea.dideric.is/", out)
# Placeholder token, not the host env var name (which is not a
# secret but also not useful) or the real value (which the
# provisioner does not have).
self.assertIn("token: cred-proxy-placeholder", out)
self.assertNotIn("GITEA_TOKEN", out)
if __name__ == "__main__":
unittest.main()