RPF-WP-0054-T05: issuer binding CCR-2026-0043 (net-kingdom-admins, aal2) and probe session
New CCR request type broker-issuer-binding: an OIDC role on netkingdom that carries only the broker issuer policy (loopback callbacks, 15m, 8 uses, no default policy); the apply refuses unless the live issuer policy matches source. Founder chose net-kingdom-admins + aal2 on 2026-10-10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 469992@bnt-lap001 Assistant-Session: e4b8e420-df0a-441d-83ae-7f19130594d9
This commit is contained in:
parent
c8022c0798
commit
b328d36a20
6 changed files with 434 additions and 6 deletions
|
|
@ -0,0 +1,72 @@
|
|||
id: CCR-2026-0043
|
||||
kind: credential-change-request
|
||||
schema_version: 1
|
||||
request_type: broker-issuer-binding
|
||||
title: Operator OIDC role for the keycape-client-secret-generate broker issuer (net-kingdom-admins, aal2)
|
||||
status: proposed
|
||||
created: '2026-10-10'
|
||||
updated: '2026-10-10'
|
||||
requester:
|
||||
agent: railiance-platform
|
||||
reason: RPF-WP-0054-T05. ops-warden (23985370) names the caller as the human operator who
|
||||
runs warden case run, using their own OIDC session. Founder chose net-kingdom-admins + aal2
|
||||
on 2026-10-10. The issuer policy is live from CCR-2026-0040.
|
||||
review:
|
||||
required: true
|
||||
required_approvers:
|
||||
- platform-owner
|
||||
- key-cape-owner
|
||||
comments: []
|
||||
target:
|
||||
domain: infotech
|
||||
tenant: platform
|
||||
workload: credential-broker
|
||||
environment: production
|
||||
purpose: Let a net-kingdom-admins member with a verified second factor log in for 15 minutes
|
||||
with only the broker issuer policy, so credential.py generate can mint the create-only
|
||||
keycape/client-secret-generate token.
|
||||
openbao:
|
||||
issuer_policy: credential-broker-keycape-client-secret-generate-issuer
|
||||
auth:
|
||||
method: oidc
|
||||
mount: netkingdom
|
||||
role: credential-broker-keycape-client-secret-generate
|
||||
role_file: openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json
|
||||
bound_claims_confirmed: false
|
||||
founder_confirmation_items:
|
||||
- bound_claims_confirmed stays false until the founder confirms groups=[net-kingdom-admins]
|
||||
and /assurance/level=aal2, the same binding as CCR-2026-0034 (key-cape b2ed890f).
|
||||
- The role carries only the issuer policy. It cannot read or write KV; it can mint
|
||||
keycape-client-secret-generate tokens through that token role, and look up and
|
||||
revoke tokens by accessor.
|
||||
- Loopback CLI callbacks only (no UI callback); 15m, 8 uses, no default policy.
|
||||
access_frontdoor:
|
||||
type: none
|
||||
catalog_id: none
|
||||
risk:
|
||||
level: medium
|
||||
notes:
|
||||
- Membership of net-kingdom-admins is the access decision; that group already holds the
|
||||
KeyCape operator reader (CCR-2026-0034).
|
||||
- Worst case within 15m and 8 uses is a few unused keycape/<client_id> entries; existing
|
||||
entries cannot be overwritten (CCR-2026-0040 is create-only).
|
||||
verification:
|
||||
positive:
|
||||
- Readback after apply matches source for the role (done by applier-apply; prints nothing).
|
||||
- A member with aal2 logs in with role credential-broker-keycape-client-secret-generate and
|
||||
the token's policies are exactly [credential-broker-keycape-client-secret-generate-issuer].
|
||||
negative:
|
||||
- A non-member or a member without aal2 cannot obtain the role.
|
||||
- The issuer token is denied on platform/* and on token creation through any other role.
|
||||
activation_conditions:
|
||||
- Founder approval (two reviews) and bound_claims_confirmed true.
|
||||
- CCR-2026-0040 applied (done 2026-10-09).
|
||||
evidence: []
|
||||
lifecycle:
|
||||
deactivate: Delete auth/netkingdom/role/credential-broker-keycape-client-secret-generate.
|
||||
rotate: Not applicable (no stored credential).
|
||||
compromised: Delete the role, revoke issued tokens; review keycape/ metadata for entries
|
||||
without an accepted case.
|
||||
state_hub:
|
||||
workplan: RPF-WP-0054
|
||||
task: RPF-WP-0054-T05
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"role_type": "oidc",
|
||||
"allowed_redirect_uris": [
|
||||
"http://localhost:8250/oidc/callback",
|
||||
"http://127.0.0.1:8250/oidc/callback"
|
||||
],
|
||||
"oidc_scopes": ["openid", "profile", "email", "groups"],
|
||||
"user_claim": "sub",
|
||||
"groups_claim": "groups",
|
||||
"bound_claims_type": "string",
|
||||
"bound_claims": {
|
||||
"groups": ["net-kingdom-admins"],
|
||||
"/assurance/level": "aal2"
|
||||
},
|
||||
"token_policies": ["credential-broker-keycape-client-secret-generate-issuer"],
|
||||
"token_no_default_policy": true,
|
||||
"token_type": "service",
|
||||
"token_ttl": "15m",
|
||||
"token_max_ttl": "15m",
|
||||
"token_explicit_max_ttl": "15m",
|
||||
"token_num_uses": 8,
|
||||
"verbose_oidc_logging": false
|
||||
}
|
||||
|
|
@ -824,6 +824,7 @@ LANE_MODULES = {
|
|||
"service-jwt-login": "service_jwt_lane",
|
||||
"workload-kv-operator-matrix": "operator_matrix_lane",
|
||||
"custody-generate-grant": "broker_grant_lane",
|
||||
"broker-issuer-binding": "issuer_binding_lane",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
165
scripts/issuer_binding_lane.py
Normal file
165
scripts/issuer_binding_lane.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""broker-issuer-binding CCRs: an attended OIDC role that carries only a broker issuer policy.
|
||||
|
||||
RPF-WP-0054-T05. Loaded by scripts/credential-change.py for request_type
|
||||
broker-issuer-binding. The role lets a human operator (group + aal2 on the
|
||||
existing netkingdom OIDC mount) hold exactly one issuer policy, so their own
|
||||
session can mint the grant token that ``credential.py generate`` uses. The role
|
||||
rules (callbacks, scopes, claims, TTLs, no default policy) are the operator-matrix
|
||||
lane's; the issuer policy must already be live from its own CCR. Nothing here
|
||||
reads, prints or accepts a secret value; the mount is never enabled here.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REQUEST_TYPE = "broker-issuer-binding"
|
||||
ISSUER_POLICY_RE = re.compile(r"^credential-broker-[a-z0-9-]+-issuer$")
|
||||
ROLE_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
|
||||
|
||||
def _load(name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, Path(__file__).resolve().parent / f"{name}.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
_matrix = _load("operator_matrix_lane")
|
||||
BaoError = _matrix.BaoError
|
||||
normalized = _matrix.normalized
|
||||
_run = _matrix._run
|
||||
_read_json = _matrix._read_json
|
||||
ALLOWED_AUTH_MOUNTS = _matrix.ALLOWED_AUTH_MOUNTS
|
||||
DURATION_KEYS = _matrix.DURATION_KEYS
|
||||
|
||||
|
||||
def role_payload(ccr: dict[str, Any], repo_dir: Path) -> dict[str, Any]:
|
||||
return _matrix.role_payload(ccr, repo_dir)
|
||||
|
||||
|
||||
def validate(ccr: dict[str, Any], repo_dir: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
openbao = ccr.get("openbao")
|
||||
if not isinstance(openbao, dict) or not isinstance(openbao.get("auth"), dict):
|
||||
return ["openbao.auth must be an object"]
|
||||
auth = openbao["auth"]
|
||||
if (ccr.get("target") or {}).get("environment") != "production":
|
||||
errors.append("target.environment must be production")
|
||||
review = ccr.get("review") or {}
|
||||
if review.get("required") is not True or not review.get("required_approvers"):
|
||||
errors.append("review.required must be true with required_approvers")
|
||||
issuer = str(openbao.get("issuer_policy") or "")
|
||||
if not ISSUER_POLICY_RE.match(issuer):
|
||||
errors.append("openbao.issuer_policy must be credential-broker-<...>-issuer")
|
||||
elif not (repo_dir / f"openbao/policies/{issuer}.hcl").exists():
|
||||
errors.append(f"openbao.issuer_policy has no source file: openbao/policies/{issuer}.hcl")
|
||||
if auth.get("method") != "oidc":
|
||||
errors.append("openbao.auth.method must be oidc")
|
||||
if auth.get("mount") not in ALLOWED_AUTH_MOUNTS:
|
||||
errors.append(f"openbao.auth.mount must be one of {sorted(ALLOWED_AUTH_MOUNTS)}")
|
||||
if not ROLE_RE.match(str(auth.get("role") or "")):
|
||||
errors.append("openbao.auth.role contains unsafe characters")
|
||||
if not isinstance(auth.get("bound_claims_confirmed"), bool):
|
||||
errors.append("openbao.auth.bound_claims_confirmed must be a boolean")
|
||||
role = _matrix._shared._load_json(repo_dir, str(auth.get("role_file") or ""), "openbao.auth.role_file", errors)
|
||||
if role:
|
||||
_matrix._validate_role(role, issuer, errors)
|
||||
return errors
|
||||
|
||||
|
||||
def readiness_blockers(ccr: dict[str, Any], repo_dir: Path, allowed_statuses: set[str]) -> list[str]:
|
||||
blockers: list[str] = []
|
||||
if ccr.get("status") not in allowed_statuses:
|
||||
blockers.append(
|
||||
"applier requires approved, applied, verified, or active CCR status, "
|
||||
f"got {ccr.get('status')}"
|
||||
)
|
||||
if ccr["openbao"]["auth"].get("bound_claims_confirmed") is not True:
|
||||
blockers.append("applier requires confirmed OpenBao auth binding")
|
||||
blockers.extend(validate(ccr, repo_dir))
|
||||
return blockers
|
||||
|
||||
|
||||
def dry_run_payload(ccr: dict[str, Any], repo_dir: Path, warnings: list[str]) -> dict[str, Any]:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
return {
|
||||
"id": ccr["id"],
|
||||
"title": ccr["title"],
|
||||
"status": ccr["status"],
|
||||
"environment": ccr["target"]["environment"],
|
||||
"warnings": warnings,
|
||||
"source_artifacts": {},
|
||||
"mutations": [
|
||||
{"kind": "issuer_policy_present", "openbao_path": f"sys/policies/acl/{openbao['issuer_policy']}",
|
||||
"payload": {"live_text_must_match": f"openbao/policies/{openbao['issuer_policy']}.hcl"}},
|
||||
{"kind": "auth_role_write", "openbao_path": f"auth/{auth['mount']}/role/{auth['role']}",
|
||||
"payload": role_payload(ccr, repo_dir),
|
||||
"precondition": f"auth/{auth['mount']} exists with type oidc (never enabled here)"},
|
||||
{"kind": "readback_compare", "openbao_path": "role", "payload": {"prints": "booleans only"}},
|
||||
],
|
||||
"out_of_scope": [
|
||||
"policy writes (the issuer policy belongs to its grant CCR)",
|
||||
"secret value reads or writes",
|
||||
"token creation or login",
|
||||
],
|
||||
"required_evidence": [
|
||||
"CCR id and approval reference",
|
||||
"readback equality for the role",
|
||||
"attended login with this role, then the RPF-WP-0054-T05 probe",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def apply_details(ccr: dict[str, Any], actor: str) -> list[str]:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
return [
|
||||
f"Delegated metadata applier ran as {actor} using local bao CLI ambient authority.",
|
||||
f"Issuer policy confirmed live and matching source: sys/policies/acl/{openbao['issuer_policy']}",
|
||||
f"OIDC role metadata write: auth/{auth['mount']}/role/{auth['role']}",
|
||||
"Readback matched source for the role.",
|
||||
"No secret values were read, written, printed, or accepted in argv.",
|
||||
]
|
||||
|
||||
|
||||
def readback_mismatches(ccr: dict[str, Any], repo_dir: Path, bao_bin: str) -> list[str]:
|
||||
auth = ccr["openbao"]["auth"]
|
||||
mismatches: list[str] = []
|
||||
role = _read_json(bao_bin, ["read", f"auth/{auth['mount']}/role/{auth['role']}"], repo_dir).get("data", {})
|
||||
for key, want in role_payload(ccr, repo_dir).items():
|
||||
got = role.get(key)
|
||||
if key in DURATION_KEYS:
|
||||
same = _matrix.ttl_seconds(got) == _matrix.ttl_seconds(want)
|
||||
elif key in ("allowed_redirect_uris", "oidc_scopes"):
|
||||
same = sorted(got or []) == sorted(want)
|
||||
else:
|
||||
same = got == want
|
||||
if not same:
|
||||
mismatches.append(f"role {key} differs")
|
||||
return mismatches
|
||||
|
||||
|
||||
def run_apply(ccr: dict[str, Any], repo_dir: Path, bao_bin: str, quiet: bool = False) -> None:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
mounts = _read_json(bao_bin, ["auth", "list"], repo_dir)
|
||||
mounts = mounts.get("data", mounts)
|
||||
if (mounts.get(f"{auth['mount']}/") or {}).get("type") != "oidc":
|
||||
raise BaoError(f"auth mount {auth['mount']}/ is absent or not oidc; this lane never enables it")
|
||||
live = _run(bao_bin, ["policy", "read", openbao["issuer_policy"]], repo_dir, quiet=True, capture=True)
|
||||
source = (repo_dir / f"openbao/policies/{openbao['issuer_policy']}.hcl").read_text(encoding="utf-8")
|
||||
if normalized(live) != normalized(source):
|
||||
raise BaoError("issuer policy is absent or differs from source; apply its grant CCR first")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
role_path = Path(tmp) / "role.json"
|
||||
role_path.write_text(json.dumps(role_payload(ccr, repo_dir), sort_keys=True) + "\n")
|
||||
_run(bao_bin, ["write", f"auth/{auth['mount']}/role/{auth['role']}", f"@{role_path}"], repo_dir, quiet)
|
||||
mismatches = readback_mismatches(ccr, repo_dir, bao_bin)
|
||||
if mismatches:
|
||||
raise BaoError("readback mismatch: " + "; ".join(mismatches))
|
||||
127
tests/test_issuer_binding_lane.py
Normal file
127
tests/test_issuer_binding_lane.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""RPF-WP-0054-T05: broker-issuer-binding lane (CCR-2026-0043)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CCR_PATH = ROOT / "credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml"
|
||||
ISSUER = "credential-broker-keycape-client-secret-generate-issuer"
|
||||
|
||||
|
||||
def load(name: str, rel: str):
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
lane = load("issuer_binding_lane", "scripts/issuer_binding_lane.py")
|
||||
cc = load("credential_change_cli_issuer", "scripts/credential-change.py")
|
||||
|
||||
|
||||
def ccr() -> dict:
|
||||
return yaml.safe_load(CCR_PATH.read_text())
|
||||
|
||||
|
||||
FAKE_BAO = r'''#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
path = os.environ["FAKE_BAO_STATE"]
|
||||
state = json.load(open(path))
|
||||
args = [a for a in sys.argv[1:] if a != "-format=json"]
|
||||
state.setdefault("calls", []).append(args)
|
||||
if args[:2] == ["auth", "list"]:
|
||||
print(json.dumps({k: {"type": v} for k, v in state["mounts"].items()}))
|
||||
elif args[:2] == ["policy", "read"]:
|
||||
if args[2] not in state["policies"]:
|
||||
json.dump(state, open(path, "w")); sys.exit(2)
|
||||
print(state["policies"][args[2]])
|
||||
elif args[0] == "write":
|
||||
state["roles"][args[1]] = json.load(open(args[2][1:]))
|
||||
elif args[0] == "read":
|
||||
print(json.dumps({"data": state["roles"][args[1]]}))
|
||||
json.dump(state, open(path, "w"))
|
||||
'''
|
||||
|
||||
|
||||
class IssuerBindingLaneTests(unittest.TestCase):
|
||||
def test_ccr_validates_and_waits_for_approval_and_binding(self):
|
||||
_ccr, errors, _w = cc.validate_ccr(CCR_PATH)
|
||||
self.assertEqual(errors, [])
|
||||
blockers = cc.applier_readiness_blockers(_ccr)
|
||||
self.assertIn("applier requires confirmed OpenBao auth binding", blockers)
|
||||
|
||||
def test_role_carries_only_the_issuer_policy(self):
|
||||
role = json.loads((ROOT / ccr()["openbao"]["auth"]["role_file"]).read_text())
|
||||
self.assertEqual(role["token_policies"], [ISSUER])
|
||||
self.assertEqual(role["bound_claims"], {"groups": ["net-kingdom-admins"], "/assurance/level": "aal2"})
|
||||
self.assertTrue(role["token_no_default_policy"])
|
||||
|
||||
def test_validator_rejects_widening(self):
|
||||
good = json.loads((ROOT / ccr()["openbao"]["auth"]["role_file"]).read_text())
|
||||
cases = [
|
||||
("other policy", {"token_policies": [ISSUER, "platform-admin"]}),
|
||||
("no aal2", {"bound_claims": {"groups": ["net-kingdom-admins"]}}),
|
||||
("public callback", {"allowed_redirect_uris": ["https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback"]}),
|
||||
("long ttl", {"token_ttl": "1h"}),
|
||||
]
|
||||
for label, change in cases:
|
||||
with self.subTest(label):
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "openbao/auth") as tmp:
|
||||
role_file = Path(tmp) / "role.json"
|
||||
role_file.write_text(json.dumps({**good, **change}))
|
||||
c = ccr()
|
||||
c["openbao"]["auth"]["role_file"] = str(role_file.relative_to(ROOT))
|
||||
self.assertNotEqual(lane.validate(c, ROOT), [], label)
|
||||
c = ccr()
|
||||
c["openbao"]["issuer_policy"] = "platform-admin"
|
||||
self.assertNotEqual(lane.validate(c, ROOT), [])
|
||||
|
||||
def _apply(self, policies, mounts=None):
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
state = {"mounts": mounts if mounts is not None else {"netkingdom/": "oidc"},
|
||||
"policies": policies, "roles": {}}
|
||||
(tmp / "state.json").write_text(json.dumps(state))
|
||||
bao = tmp / "bao"
|
||||
bao.write_text(FAKE_BAO)
|
||||
bao.chmod(bao.stat().st_mode | stat.S_IEXEC)
|
||||
os.environ["FAKE_BAO_STATE"] = str(tmp / "state.json")
|
||||
error = None
|
||||
try:
|
||||
lane.run_apply(ccr(), ROOT, str(bao), quiet=True)
|
||||
except lane.BaoError as exc:
|
||||
error = exc
|
||||
return error, json.loads((tmp / "state.json").read_text())
|
||||
|
||||
def live_issuer(self):
|
||||
return {ISSUER: (ROOT / f"openbao/policies/{ISSUER}.hcl").read_text()}
|
||||
|
||||
def test_apply_writes_role_and_reads_back(self):
|
||||
error, state = self._apply(self.live_issuer())
|
||||
self.assertIsNone(error)
|
||||
self.assertIn("auth/netkingdom/role/credential-broker-keycape-client-secret-generate", state["roles"])
|
||||
|
||||
def test_apply_refuses_missing_or_drifted_issuer_policy_before_writing(self):
|
||||
for policies in ({}, {ISSUER: 'path "platform/*" { capabilities = ["read"] }'}):
|
||||
with self.subTest(policies=list(policies)):
|
||||
error, state = self._apply(policies)
|
||||
self.assertIsNotNone(error)
|
||||
self.assertEqual(state["roles"], {})
|
||||
|
||||
def test_apply_refuses_non_oidc_mount(self):
|
||||
error, state = self._apply(self.live_issuer(), {"netkingdom/": "jwt"})
|
||||
self.assertIsNotNone(error)
|
||||
self.assertEqual(state["roles"], {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -211,12 +211,52 @@ State Hub evidence step (KeyError on `openbao.auth` for a prefix lane), so warde
|
|||
revoked the session. That was fixed and the hub event was posted afterwards. The
|
||||
OpenBao objects were complete before the crash.
|
||||
|
||||
After T01 and T03 (separate session): review and apply the issuer binding for
|
||||
the named caller, then run `generate` once for a disposable client_id
|
||||
`probe-secret-generate-<date>`. Verify by metadata only that it is version 1.
|
||||
Confirm that a second run refuses, and that the grant token is denied a data
|
||||
read, `update`, `delete` and every path outside `keycape/`. Metadata-delete the
|
||||
probe.
|
||||
2026-10-10: the founder chose `net-kingdom-admins` + `aal2` for the caller.
|
||||
CCR-2026-0043 (request type `broker-issuer-binding`,
|
||||
`scripts/issuer_binding_lane.py`) adds OIDC role
|
||||
`credential-broker-keycape-client-secret-generate` on `auth/netkingdom`. It
|
||||
carries only the issuer policy and has loopback callbacks, a 15m TTL, 8 uses and
|
||||
no default policy. The apply refuses unless the live issuer policy matches source.
|
||||
|
||||
Founder session (probe uses a synthetic, founder-written case file; the real
|
||||
Forgejo run waits for key-cape's format confirmation):
|
||||
|
||||
```bash
|
||||
cd ~/railiance-platform
|
||||
export BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200
|
||||
# 1. binding
|
||||
python3 scripts/credential-change.py approve CCR-2026-0043 --reviewer "platform-owner, key-cape-owner (founder Bernd Worsch)" --comment "Approve issuer binding for keycape-client-secret-generate: net-kingdom-admins + aal2 (RPF-WP-0054)." \
|
||||
&& python3 scripts/credential-change.py confirm-binding CCR-2026-0043 --reviewer founder --comment "groups=[net-kingdom-admins] and /assurance/level=aal2, same binding as CCR-2026-0034." \
|
||||
&& warden access openbao-platform-admin-login --exec -- python3 scripts/credential-change.py applier-apply CCR-2026-0043 --confirm "DELEGATED APPLY CCR-2026-0043" --actor founder --record-state-hub --quiet
|
||||
# 2. probe case (synthetic, materialized, never registered in KeyCape)
|
||||
P=probe-secret-generate-20261010
|
||||
cat > ~/$P-case.yaml <<YAML
|
||||
apiVersion: netkingdom.io/security-case/v0.1
|
||||
kind: SecurityCase
|
||||
spec:
|
||||
id: $P
|
||||
type: oidc-browser-relying-party@1.0
|
||||
status: materialized
|
||||
parameters:
|
||||
client_id: $P
|
||||
client_type: confidential
|
||||
YAML
|
||||
REF="$P@sha256:$(sha256sum ~/$P-case.yaml | cut -d' ' -f1)"
|
||||
# 3. log in as the bound operator role (token goes to the bao token helper)
|
||||
bao login -no-print -method=oidc -path=netkingdom role=credential-broker-keycape-client-secret-generate
|
||||
G="python3 scripts/credential.py generate --grant keycape/client-secret-generate --client-id $P --case-file $HOME/$P-case.yaml --case-ref $REF"
|
||||
$G --dry-run --receipt ~/$P-dry.json
|
||||
$G --receipt ~/$P-1.json # expect generated, kv_version 1
|
||||
$G --receipt ~/$P-2.json # expect exit 3, already_present
|
||||
bao token revoke -self
|
||||
cat ~/$P-dry.json ~/$P-1.json ~/$P-2.json
|
||||
# 4. remove the probe entry
|
||||
warden access openbao-platform-admin-login --exec -- sh -c "bao delete platform/metadata/workloads/keycape/$P >/dev/null 2>&1"
|
||||
```
|
||||
|
||||
The policy denials (data read, update, delete, other paths) were already proven
|
||||
by the CCR-2026-0040 readback and its policy tests. The probe proves the live
|
||||
create-once behaviour.
|
||||
|
||||
Record the receipts under `docs/evidence/`.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue