Activate Policy Nexus source credential lane
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
This commit is contained in:
parent
a6d47c51cc
commit
62423fd092
3 changed files with 207 additions and 7 deletions
|
|
@ -3,7 +3,7 @@ kind: credential-change-request
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
request_type: workload-kv-read
|
request_type: workload-kv-read
|
||||||
title: Policy Nexus Forgejo private-source read token lane
|
title: Policy Nexus Forgejo private-source read token lane
|
||||||
status: applied
|
status: active
|
||||||
created: '2026-08-31'
|
created: '2026-08-31'
|
||||||
updated: '2026-08-31'
|
updated: '2026-08-31'
|
||||||
requester:
|
requester:
|
||||||
|
|
@ -75,8 +75,8 @@ access_frontdoor:
|
||||||
type: ops-warden
|
type: ops-warden
|
||||||
catalog_id: policy-nexus-forgejo-source-read
|
catalog_id: policy-nexus-forgejo-source-read
|
||||||
selector: policy nexus Forgejo private source repository read token Actions
|
selector: policy nexus Forgejo private source repository read token Actions
|
||||||
readiness: pending-review
|
readiness: ready
|
||||||
resolvable: false
|
resolvable: true
|
||||||
delivery:
|
delivery:
|
||||||
surface: forgejo-actions-secret
|
surface: forgejo-actions-secret
|
||||||
bootstrap_command: warden access openbao-platform-admin-login --exec -- scripts/openbao-bootstrap-policy-nexus-source.sh
|
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'
|
- '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'
|
- '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.
|
- 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:
|
lifecycle:
|
||||||
deactivate: Remove the repository Actions secret, revoke the Forgejo PAT, disable
|
deactivate: Remove the repository Actions secret, revoke the Forgejo PAT, disable
|
||||||
the OpenBao access path, and leave scheduled publication failing closed.
|
the OpenBao access path, and leave scheduled publication failing closed.
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -52,6 +54,33 @@ def write_diagnostic(stage: str, error: Exception | None = None) -> None:
|
||||||
DIAGNOSTIC_PATH.chmod(0o600)
|
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,}", "<redacted>", 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(
|
def api_request(
|
||||||
token: str,
|
token: str,
|
||||||
method: str,
|
method: str,
|
||||||
|
|
@ -60,6 +89,8 @@ def api_request(
|
||||||
payload: dict[str, object] | None = None,
|
payload: dict[str, object] | None = None,
|
||||||
expected: tuple[int, ...] = (200,),
|
expected: tuple[int, ...] = (200,),
|
||||||
read_body: bool = True,
|
read_body: bool = True,
|
||||||
|
extra_headers: dict[str, str] | None = None,
|
||||||
|
authorization: str | None = None,
|
||||||
) -> tuple[int, object | None]:
|
) -> tuple[int, object | None]:
|
||||||
data = None if payload is None else json.dumps(payload).encode()
|
data = None if payload is None else json.dumps(payload).encode()
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
|
|
@ -67,9 +98,10 @@ def api_request(
|
||||||
data=data,
|
data=data,
|
||||||
method=method,
|
method=method,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"token {token}",
|
"Authorization": authorization or f"token {token}",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
**({"Content-Type": "application/json"} if data is not None else {}),
|
**({"Content-Type": "application/json"} if data is not None else {}),
|
||||||
|
**(extra_headers or {}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
@ -267,15 +299,41 @@ def provision(admin_token: str) -> None:
|
||||||
token_name = TOKEN_PREFIX + timestamp
|
token_name = TOKEN_PREFIX + timestamp
|
||||||
token_id: int | None = None
|
token_id: int | None = None
|
||||||
source_token = ""
|
source_token = ""
|
||||||
|
service_password = ""
|
||||||
|
basic_authorization = ""
|
||||||
bao_written = False
|
bao_written = False
|
||||||
activated = False
|
activated = False
|
||||||
try:
|
try:
|
||||||
_, created = api_request(
|
service_password = secrets.token_urlsafe(48)
|
||||||
|
api_request(
|
||||||
admin_token,
|
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",
|
"POST",
|
||||||
f"/users/{USER}/tokens",
|
f"/users/{USER}/tokens",
|
||||||
payload={"name": token_name, "scopes": SCOPES},
|
payload={"name": token_name, "scopes": SCOPES},
|
||||||
expected=(201,),
|
expected=(201,),
|
||||||
|
authorization=basic_authorization,
|
||||||
)
|
)
|
||||||
if not isinstance(created, dict) or not isinstance(created.get("id"), int):
|
if not isinstance(created, dict) or not isinstance(created.get("id"), int):
|
||||||
raise ProvisionError("Forgejo returned an invalid access-token record")
|
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:
|
if not activated and token_id is not None:
|
||||||
try:
|
try:
|
||||||
api_request(
|
api_request(
|
||||||
admin_token,
|
"",
|
||||||
"DELETE",
|
"DELETE",
|
||||||
f"/users/{USER}/tokens/{token_id}",
|
f"/users/{USER}/tokens/{token_id}",
|
||||||
expected=(204, 404),
|
expected=(204, 404),
|
||||||
|
authorization=basic_authorization,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -338,6 +397,8 @@ def provision(admin_token: str) -> None:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
source_token = ""
|
source_token = ""
|
||||||
|
service_password = ""
|
||||||
|
basic_authorization = ""
|
||||||
|
|
||||||
|
|
||||||
def outer() -> int:
|
def outer() -> int:
|
||||||
|
|
@ -361,7 +422,10 @@ def outer() -> int:
|
||||||
timeout=900,
|
timeout=900,
|
||||||
)
|
)
|
||||||
if result.returncode != 0 and not DIAGNOSTIC_PATH.exists():
|
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
|
return result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,25 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase):
|
||||||
self.assertIn("repo.actions", MODULE.NON_CODE_UNITS)
|
self.assertIn("repo.actions", MODULE.NON_CODE_UNITS)
|
||||||
self.assertIn("repo.packages", 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:
|
def test_broader_returned_scope_is_rejected_before_network_checks(self) -> None:
|
||||||
with mock.patch.object(MODULE, "api_request") as request:
|
with mock.patch.object(MODULE, "api_request") as request:
|
||||||
with self.assertRaises(MODULE.ProvisionError):
|
with self.assertRaises(MODULE.ProvisionError):
|
||||||
|
|
@ -48,6 +67,28 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase):
|
||||||
payload = path.write_text.call_args.args[0]
|
payload = path.write_text.call_args.args[0]
|
||||||
self.assertIn("PUT /bounded/path returned HTTP 403", payload)
|
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:
|
def test_actions_failure_revokes_pat_and_removes_new_kv_value(self) -> None:
|
||||||
calls: list[tuple[str, str]] = []
|
calls: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
|
@ -57,6 +98,8 @@ class PolicyNexusForgejoSourceProvisionTests(unittest.TestCase):
|
||||||
return 200, []
|
return 200, []
|
||||||
if path.endswith("/actions/secrets?limit=100"):
|
if path.endswith("/actions/secrets?limit=100"):
|
||||||
return 200, []
|
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":
|
if method == "POST" and path == "/users/policy-nexus-source/tokens":
|
||||||
return 201, {
|
return 201, {
|
||||||
"id": 73,
|
"id": 73,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue