diff --git a/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml b/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml index ca882b8..0553370 100644 --- a/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml +++ b/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml @@ -3,7 +3,7 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: Policy Nexus Forgejo private-source read token lane -status: applied +status: active created: '2026-08-31' updated: '2026-08-31' requester: @@ -75,8 +75,8 @@ access_frontdoor: type: ops-warden catalog_id: policy-nexus-forgejo-source-read selector: policy nexus Forgejo private source repository read token Actions - readiness: pending-review - resolvable: false + readiness: ready + resolvable: true delivery: surface: forgejo-actions-secret bootstrap_command: warden access openbao-platform-admin-login --exec -- scripts/openbao-bootstrap-policy-nexus-source.sh @@ -147,6 +147,99 @@ verification: - 'Policy metadata write: sys/policies/acl/workload-kv-read-policy-nexus-forgejo-source' - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:19:13+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:20:52+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:22:40+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:24:53+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:27:18+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:29:29+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-policy-nexus-forgejo-source' + - 'Auth role metadata write: auth/netkingdom/role/policy-nexus-forgejo-source-workload-kv-read' + - No secret values were read, written, printed, or accepted in argv. + - at: '2026-08-31T22:29:36+00:00' + actor: attended operator via governed platform-admin lane + kind: forgejo_source_bootstrap + result: passed + details: + - Restricted user policy-nexus-source and all-repository repo.code-read team policy-nexus-source-readers + verified; PAT policy-nexus-source-read-20260831T222932Z reports exactly read:repository; + archive read passed; effective repository permission is read-only; Actions-secret + read and instance-admin probes were denied; OpenBao and repository Actions secret + were populated without value output; workflow dispatch run_id=3802. + - at: '2026-08-31T23:28:57+00:00' + actor: codex via verified Forgejo Actions + kind: workflow_verification + result: passed + details: + - Forgejo Actions run 32 succeeded for exact Policy Nexus commit 1e6720b9eeba819b42133afcdfb9693608cddb42 + after resolving the complete private-source inventory and publishing 65 documents. + - Candidate OCI digest sha256:a3a2b7b8b9432535771b588b9c78c554924ec1adc6d5a9bbc1c35b28fea6b724; + publication manifest 0af785af708b3698bb7af89aacf246be93031c69566180699c7dfaf1b3e549d3; + source inventory 2ff18298f0bd8ce75ca3adcdf09323f136a8ff71a2d55a2e43556d55a0bbf250; + source set 5f9e0bb9c19f95927c6bee96ad72f5446cb9a28f188effafe443446c5c07b0b0. + - The run used the repository Actions secret and emitted no credential value. + - at: '2026-08-31T23:29:23+00:00' + actor: codex via installed ops-warden CLI + kind: frontdoor_activation + result: passed + details: + - Installed Warden catalog route policy-nexus-forgejo-source-read is active, exact, + high-risk, and resolvable with no placeholders. + - Warden route and plan select the exact OpenBao path and FORGEJO_SOURCE_TOKEN + field; agent callers are limited to sanctioned exec, out, or wrap transports. + - 'The complete ops-warden suite passed: 406 selected tests, including the generated + high-risk data-path boundary.' lifecycle: deactivate: Remove the repository Actions secret, revoke the Forgejo PAT, disable the OpenBao access path, and leave scheduled publication failing closed. diff --git a/scripts/provision-policy-nexus-forgejo-source.py b/scripts/provision-policy-nexus-forgejo-source.py index 0eae946..13bcf3b 100755 --- a/scripts/provision-policy-nexus-forgejo-source.py +++ b/scripts/provision-policy-nexus-forgejo-source.py @@ -4,10 +4,12 @@ from __future__ import annotations import argparse +import base64 import datetime as dt import json import os from pathlib import Path +import re import secrets import subprocess import sys @@ -52,6 +54,33 @@ def write_diagnostic(stage: str, error: Exception | None = None) -> None: DIAGNOSTIC_PATH.chmod(0o600) +def classify_warden_failure(result: subprocess.CompletedProcess[bytes]) -> str: + output = (result.stdout or b"") + b"\n" + (result.stderr or b"") + text = output.decode("utf-8", errors="replace").lower() + checks = ( + ("routing catalog", "routing-catalog"), + ("caller auth", "caller-auth"), + ("permission denied", "owner-permission"), + ("fetch failed", "owner-fetch"), + ("policy denied", "policy-denied"), + ("founder_required", "founder-required"), + ("not valid", "invalid-invocation"), + ("requires --", "missing-argument"), + ) + for needle, classification in checks: + if needle in text: + return classification + return "unclassified" + + +def sanitize_warden_failure(result: subprocess.CompletedProcess[bytes]) -> str: + output = (result.stdout or b"") + b"\n" + (result.stderr or b"") + text = output.decode("utf-8", errors="replace") + text = re.sub(r"[A-Za-z0-9_=.:-]{20,}", "", text) + lines = [line.strip() for line in text.splitlines() if line.strip()] + return " | ".join(lines[-8:])[:600] or f"nested Warden exit {result.returncode}" + + def api_request( token: str, method: str, @@ -60,6 +89,8 @@ def api_request( payload: dict[str, object] | None = None, expected: tuple[int, ...] = (200,), read_body: bool = True, + extra_headers: dict[str, str] | None = None, + authorization: str | None = None, ) -> tuple[int, object | None]: data = None if payload is None else json.dumps(payload).encode() request = urllib.request.Request( @@ -67,9 +98,10 @@ def api_request( data=data, method=method, headers={ - "Authorization": f"token {token}", + "Authorization": authorization or f"token {token}", "Accept": "application/json", **({"Content-Type": "application/json"} if data is not None else {}), + **(extra_headers or {}), }, ) try: @@ -267,15 +299,41 @@ def provision(admin_token: str) -> None: token_name = TOKEN_PREFIX + timestamp token_id: int | None = None source_token = "" + service_password = "" + basic_authorization = "" bao_written = False activated = False try: - _, created = api_request( + service_password = secrets.token_urlsafe(48) + api_request( admin_token, + "PATCH", + f"/admin/users/{USER}", + payload={ + "password": service_password, + "must_change_password": False, + "restricted": True, + "admin": False, + "allow_create_organization": False, + "allow_git_hook": False, + "allow_import_local": False, + "max_repo_creation": 0, + "prohibit_login": False, + "active": True, + "visibility": "private", + }, + expected=(200,), + ) + basic = base64.b64encode(f"{USER}:{service_password}".encode()).decode() + basic_authorization = f"Basic {basic}" + basic = "" + _, created = api_request( + "", "POST", f"/users/{USER}/tokens", payload={"name": token_name, "scopes": SCOPES}, expected=(201,), + authorization=basic_authorization, ) if not isinstance(created, dict) or not isinstance(created.get("id"), int): raise ProvisionError("Forgejo returned an invalid access-token record") @@ -326,10 +384,11 @@ def provision(admin_token: str) -> None: if not activated and token_id is not None: try: api_request( - admin_token, + "", "DELETE", f"/users/{USER}/tokens/{token_id}", expected=(204, 404), + authorization=basic_authorization, ) except Exception: pass @@ -338,6 +397,8 @@ def provision(admin_token: str) -> None: raise finally: source_token = "" + service_password = "" + basic_authorization = "" def outer() -> int: @@ -361,7 +422,10 @@ def outer() -> int: timeout=900, ) if result.returncode != 0 and not DIAGNOSTIC_PATH.exists(): - write_diagnostic("forgejo-admin-route") + write_diagnostic( + f"forgejo-admin-route:{classify_warden_failure(result)}", + ProvisionError(sanitize_warden_failure(result)), + ) return result.returncode diff --git a/tests/test_policy_nexus_forgejo_source_provision.py b/tests/test_policy_nexus_forgejo_source_provision.py index e1caa16..ffb7551 100644 --- a/tests/test_policy_nexus_forgejo_source_provision.py +++ b/tests/test_policy_nexus_forgejo_source_provision.py @@ -25,6 +25,25 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase): self.assertIn("repo.actions", MODULE.NON_CODE_UNITS) self.assertIn("repo.packages", MODULE.NON_CODE_UNITS) + def test_token_creation_accepts_target_user_basic_auth(self) -> None: + with mock.patch.object(MODULE.urllib.request, "urlopen") as urlopen: + response = mock.MagicMock() + response.status = 201 + response.read.return_value = ( + b'{"id": 1, "sha1": "candidate", "scopes": ["read:repository"]}' + ) + urlopen.return_value.__enter__.return_value = response + MODULE.api_request( + "", + "POST", + "/users/policy-nexus-source/tokens", + payload={"name": "candidate", "scopes": ["read:repository"]}, + expected=(201,), + authorization="Basic non-production", + ) + request = urlopen.call_args.args[0] + self.assertEqual(request.get_header("Authorization"), "Basic non-production") + def test_broader_returned_scope_is_rejected_before_network_checks(self) -> None: with mock.patch.object(MODULE, "api_request") as request: with self.assertRaises(MODULE.ProvisionError): @@ -48,6 +67,28 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase): payload = path.write_text.call_args.args[0] self.assertIn("PUT /bounded/path returned HTTP 403", payload) + def test_warden_failure_classification_is_allowlisted(self) -> None: + result = subprocess.CompletedProcess( + ["warden"], + 2, + stdout=b"opaque provider material\n", + stderr=b"fetch failed (exit 2) - check caller auth and the path\n", + ) + classification = MODULE.classify_warden_failure(result) + self.assertEqual(classification, "caller-auth") + self.assertNotIn("opaque provider material", classification) + + def test_warden_failure_summary_redacts_token_like_strings(self) -> None: + result = subprocess.CompletedProcess( + ["warden"], + 2, + stdout=b"route failed for hvs.NONPRODUCTION_SECRET_SENTINEL\n", + stderr=b"invalid invocation\n", + ) + summary = MODULE.sanitize_warden_failure(result) + self.assertIn("invalid invocation", summary) + self.assertNotIn("NONPRODUCTION_SECRET_SENTINEL", summary) + def test_actions_failure_revokes_pat_and_removes_new_kv_value(self) -> None: calls: list[tuple[str, str]] = [] @@ -57,6 +98,8 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase): return 200, [] if path.endswith("/actions/secrets?limit=100"): return 200, [] + if method == "PATCH" and path == "/admin/users/policy-nexus-source": + return 200, {"login": "policy-nexus-source", "restricted": True} if method == "POST" and path == "/users/policy-nexus-source/tokens": return 201, { "id": 73,