CCR-2026-0031: choose the private probe repo with the admin token
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

A read:repository-only PAT gets HTTP 403 on GET /orgs/coulomb/repos (needs
read:organization). The 2026-10-03 attended run failed there and rolled back
(PAT deleted, nothing written to OpenBao). The source token is still verified
against the probe exactly as before: branch, archive, /admin/users and Actions
secrets denials. Also records the delegated metadata apply evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3775@bnt-lap001
Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
This commit is contained in:
codex 2026-10-03 10:07:54 +02:00
parent 790aab025f
commit f15ff61362
3 changed files with 55 additions and 9 deletions

View file

@ -3,7 +3,7 @@ kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: Core Hub repo-manager publisher Forgejo private-source read token lane
status: approved
status: applied
created: '2026-10-03'
updated: '2026-10-03'
requester:
@ -142,7 +142,37 @@ verification:
- The ClusterSecretStore and ExternalSecret are committed and synced; rapp-core-hub
mounts the Secret and passes the positive and negative checks above.
- ops-warden catalog entry core-hub-forgejo-source-read is added and reviewed.
evidence: []
evidence:
- at: '2026-10-03T06:40:16+00:00'
actor: attended operator via governed platform-admin lane
kind: delegated_metadata_apply
result: passed
details:
- Delegated metadata applier ran as attended operator via governed platform-admin
lane using local bao CLI ambient authority.
- 'Policy metadata write: sys/policies/acl/workload-kv-read-core-hub-forgejo-source'
- 'Auth role metadata write: auth/kubernetes/role/external-secrets-core-hub-forgejo-source'
- No secret values were read, written, printed, or accepted in argv.
- at: '2026-10-03T08:06:12+00:00'
actor: attended operator via governed platform-admin lane
kind: delegated_metadata_apply
result: passed
details:
- Delegated metadata applier ran as attended operator via governed platform-admin
lane using local bao CLI ambient authority.
- 'Policy metadata write: sys/policies/acl/workload-kv-read-core-hub-forgejo-source'
- 'Auth role metadata write: auth/kubernetes/role/external-secrets-core-hub-forgejo-source'
- No secret values were read, written, printed, or accepted in argv.
- at: '2026-10-03T08:06:37+00:00'
actor: attended operator via governed platform-admin lane
kind: delegated_metadata_apply
result: passed
details:
- Delegated metadata applier ran as attended operator via governed platform-admin
lane using local bao CLI ambient authority.
- 'Policy metadata write: sys/policies/acl/workload-kv-read-core-hub-forgejo-source'
- 'Auth role metadata write: auth/kubernetes/role/external-secrets-core-hub-forgejo-source'
- No secret values were read, written, printed, or accepted in argv.
lifecycle:
deactivate: Delete ExternalSecret and Secret in core-hub, unset publisher.existingSecret,
revoke the PAT, disable the auth role, and leave private sources failing closed.

View file

@ -193,9 +193,15 @@ def ensure_team(admin_token: str) -> int:
return team_id
def find_private_probe(source_token: str) -> str:
def find_private_probe(admin_token: str) -> str:
"""Pick a private org repository to probe.
Listing org repositories needs read:organization, which the least-privilege
source token deliberately lacks (Forgejo returns 403), so the admin token
chooses the probe and the source token is then tested against it.
"""
for page in range(1, 6):
_, repos = api_request(source_token, "GET", f"/orgs/{ORG}/repos?limit=50&page={page}")
_, repos = api_request(admin_token, "GET", f"/orgs/{ORG}/repos?limit=50&page={page}")
if not isinstance(repos, list):
raise ProvisionError("Forgejo returned an invalid repository list")
for repo in repos:
@ -203,13 +209,12 @@ def find_private_probe(source_token: str) -> str:
return str(repo["name"])
if len(repos) < 50:
break
raise ProvisionError("source token sees no private repository in the org")
raise ProvisionError("no private repository found in the org to probe")
def verify_source_token(source_token: str, scopes: object) -> str:
def verify_source_token(source_token: str, scopes: object, probe: str) -> str:
if scopes != SCOPES:
raise ProvisionError("Forgejo returned scopes other than exactly read:repository")
probe = find_private_probe(source_token)
_, repo = api_request(source_token, "GET", f"/repos/{ORG}/{probe}")
branch_name = repo.get("default_branch") if isinstance(repo, dict) else None
if not isinstance(branch_name, str) or not branch_name:
@ -304,7 +309,7 @@ def provision(admin_token: str) -> str:
source_token = str(created.get("sha1") or "")
if not source_token:
raise ProvisionError("Forgejo did not return the new access token")
probe = verify_source_token(source_token, created.get("scopes"))
probe = verify_source_token(source_token, created.get("scopes"), find_private_probe(admin_token))
verify_effective_permission(admin_token, probe)
stored = bao(

View file

@ -60,6 +60,7 @@ class Harness:
with (
mock.patch.object(MODULE, "ensure_user"),
mock.patch.object(MODULE, "ensure_team", return_value=7),
mock.patch.object(MODULE, "find_private_probe", return_value="private-probe") as probe_finder,
mock.patch.object(MODULE, "verify_source_token", return_value="private-probe"),
mock.patch.object(MODULE, "verify_effective_permission"),
mock.patch.object(MODULE, "record_evidence") as evidence,
@ -67,6 +68,7 @@ class Harness:
mock.patch.object(MODULE, "bao", side_effect=self.bao),
):
self.evidence = evidence
self.probe_finder = probe_finder
return MODULE.provision("admin-token")
def revoked(self):
@ -101,7 +103,7 @@ class ContractTests(unittest.TestCase):
def test_broader_scope_rejected_before_network(self) -> None:
with mock.patch.object(MODULE, "api_request") as request:
with self.assertRaises(MODULE.ProvisionError):
MODULE.verify_source_token(SENTINEL, ["read:repository", "write:repository"])
MODULE.verify_source_token(SENTINEL, ["read:repository", "write:repository"], "priv")
request.assert_not_called()
def test_errors_never_carry_query_strings_or_unexpected_detail(self) -> None:
@ -123,6 +125,15 @@ class ContractTests(unittest.TestCase):
self.assertEqual(MODULE.find_private_probe(SENTINEL), "priv")
def test_probe_is_listed_with_the_admin_token_not_the_source_token(self) -> None:
"""2026-10-03: a read:repository-only PAT gets 403 on /orgs/{org}/repos."""
repos = [{"name": "priv", "private": True}]
with mock.patch.object(MODULE, "api_request", return_value=(200, repos)) as request:
self.assertEqual(MODULE.find_private_probe("admin-token"), "priv")
self.assertEqual(request.call_args.args[0], "admin-token")
self.assertTrue(request.call_args.args[2].startswith("/orgs/coulomb/repos"))
class ProvisionFlowTests(unittest.TestCase):
def test_fresh_mint_writes_value_only_via_stdin(self) -> None:
h = Harness()