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.
This commit is contained in:
@@ -7,6 +7,7 @@ from claude_bottle.cred_proxy_server import (
|
||||
Route,
|
||||
build_forward_headers,
|
||||
filter_response_headers,
|
||||
is_git_push_request,
|
||||
load_tokens,
|
||||
parse_routes,
|
||||
select_route,
|
||||
@@ -183,6 +184,49 @@ class TestFilterResponseHeaders(unittest.TestCase):
|
||||
self.assertNotIn("transfer-encoding", names)
|
||||
|
||||
|
||||
class TestIsGitPushRequest(unittest.TestCase):
|
||||
"""git push over HTTPS goes through /info/refs?service=git-receive-pack
|
||||
(capabilities probe) then POST /git-receive-pack (the push body).
|
||||
Fetches use /git-upload-pack and are not blocked — the bypass we're
|
||||
closing is push, since git-gate's gitleaks pre-receive is the scanner
|
||||
for outbound git data."""
|
||||
|
||||
def test_push_capabilities_probe_blocked(self):
|
||||
self.assertTrue(is_git_push_request(
|
||||
"/gh-git/owner/repo.git/info/refs",
|
||||
"service=git-receive-pack",
|
||||
))
|
||||
|
||||
def test_push_body_blocked(self):
|
||||
self.assertTrue(is_git_push_request(
|
||||
"/gh-git/owner/repo.git/git-receive-pack", "",
|
||||
))
|
||||
|
||||
def test_fetch_capabilities_allowed(self):
|
||||
self.assertFalse(is_git_push_request(
|
||||
"/gh-git/owner/repo.git/info/refs",
|
||||
"service=git-upload-pack",
|
||||
))
|
||||
|
||||
def test_fetch_body_allowed(self):
|
||||
self.assertFalse(is_git_push_request(
|
||||
"/gh-git/owner/repo.git/git-upload-pack", "",
|
||||
))
|
||||
|
||||
def test_rest_api_allowed(self):
|
||||
# tea/gh-style REST calls hit /api/v1/... — unrelated.
|
||||
self.assertFalse(is_git_push_request(
|
||||
"/gitea/gitea.dideric.is/api/v1/repos/x/y", "",
|
||||
))
|
||||
|
||||
def test_push_with_extra_query_params(self):
|
||||
# `service` may appear with other params in any order.
|
||||
self.assertTrue(is_git_push_request(
|
||||
"/gh-git/owner/repo.git/info/refs",
|
||||
"trace=1&service=git-receive-pack",
|
||||
))
|
||||
|
||||
|
||||
class TestLoadTokens(unittest.TestCase):
|
||||
def test_reads_per_route_env(self):
|
||||
routes = (
|
||||
|
||||
Reference in New Issue
Block a user