Revise KeyCape secrets-engine-openbao custody: OpenBao-only, operator reader CCR-2026-0034 (source only)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Founder direction 2026-10-06 and ops-mason plan keycape-secrets-engine-openbao-client-custody
(658cdbc, MASON-WP-0009): the client secret lives only in OpenBao.

- CCR-2026-0033: consumer file and secrets-engine D1-D4 removed; KV metadata declared
  (cas_required, max_versions=2, delete_version_after=0s, custom_metadata); target.domain
  infotech; live order and roll-forward rollback revised; status stays proposed.
- New sibling CCR-2026-0034 + policy: attended OIDC operator reader on auth/netkingdom,
  read data path + revoke-self only, 5m/4 uses, no default policy, bound
  groups=net-kingdom-admins and /assurance/level=aal2 (KeyCape 7d96ee4). Proposed,
  bound claims unconfirmed (founder item for both readers).
- credential-change.py: OIDC roles carry token_max_ttl/explicit_max_ttl/no_default_policy/
  num_uses/type and bound_claims_type (string only); token_revoke_self policy option;
  validated openbao.kv_metadata. Tests added.
- Custody child: consumer write and check-secret-file removed; cas=0 create / --rotate
  cas=<current>; compare-before-write metadata put; refusals unchanged.
- Founder runbook (keycape/founder-runbook.md), window docs, RPF-WP-0035-T09 (waits on A1/A2/A3).

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 132360@bnt-lap001
Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
This commit is contained in:
codex 2026-10-06 14:26:42 +02:00
parent 9e23eaa067
commit 4a284f0a3b
11 changed files with 734 additions and 307 deletions

View file

@ -453,6 +453,65 @@ class CredentialChangeTests(unittest.TestCase):
["openid", "profile", "email", "groups"],
)
def test_oidc_operator_reader_carries_token_bounds_and_assurance(self) -> None:
path = REPO_DIR / "credential-change-requests/CCR-2026-0034.yaml"
ccr, errors, _warnings = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
payload = credential_change.auth_payload(ccr)
self.assertEqual(payload["token_num_uses"], 4)
self.assertEqual((payload["ttl"], payload["token_max_ttl"], payload["token_explicit_max_ttl"]),
("5m", "5m", "5m"))
self.assertIs(payload["token_no_default_policy"], True)
self.assertEqual(payload["token_type"], "service")
self.assertEqual(payload["bound_claims_type"], "string")
self.assertEqual(payload["bound_claims"],
{"groups": ["net-kingdom-admins"], "/assurance/level": "aal2"})
body = credential_change.generated_policy_hcl(ccr)
self.assertIn('path "auth/token/revoke-self"', body)
self.assertNotIn("lookup-self", body)
self.assertNotIn("/metadata/", body)
self.assertEqual(credential_change.policy_artifact_diff(ccr)["status"], "matches")
self.assertEqual(credential_change.applier_policy_violations(ccr), [])
def test_oidc_token_bounds_and_kv_metadata_are_validated(self) -> None:
import yaml
source = yaml.safe_load((REPO_DIR / "credential-change-requests/CCR-2026-0034.yaml").read_text())
cases = [
(("openbao", "auth", "token_num_uses"), 0, "token_num_uses must be a positive integer"),
(("openbao", "auth", "token_num_uses"), True, "token_num_uses must be a positive integer"),
(("openbao", "auth", "token_type"), "root", "token_type must be one of"),
(("openbao", "auth", "bound_claims_type"), "glob", "glob matching is not admitted"),
(("openbao", "auth", "token_max_ttl"), "5 minutes", "token_max_ttl must be a TTL string"),
(("openbao", "kv_metadata"), {"max_versions": 0}, "max_versions must be a positive integer"),
(("openbao", "kv_metadata"), {"delete_version_after": "never"}, "delete_version_after"),
(("openbao", "kv_metadata"), {"versions": 2}, "unknown keys"),
(("openbao", "kv_metadata"), {"custom_metadata": {"a": 1}}, "custom_metadata must map"),
]
with tempfile.TemporaryDirectory() as tmp:
for keys, value, message in cases:
ccr = yaml.safe_load(yaml.safe_dump(source))
target = ccr
for key in keys[:-1]:
target = target[key]
target[keys[-1]] = value
path = Path(tmp) / "CCR-2026-0034.yaml"
path.write_text(yaml.safe_dump(ccr))
_ccr, errors, _ = credential_change.validate_ccr(path)
self.assertTrue(any(message in error for error in errors), (keys, value, errors))
def test_keycape_secrets_engine_openbao_ccr_declares_kv_metadata_without_consumer_copy(self) -> None:
path = REPO_DIR / "credential-change-requests/CCR-2026-0033.yaml"
ccr, errors, _ = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
self.assertNotIn("consumer_copy", ccr)
self.assertEqual(ccr["target"]["domain"], "infotech")
self.assertEqual(ccr["status"], "proposed")
meta = ccr["openbao"]["kv_metadata"]
self.assertEqual((meta["cas_required"], meta["max_versions"], meta["delete_version_after"]),
(True, 2, "0s"))
self.assertEqual(meta["custom_metadata"]["readers"], "eso-sso-issuer,oidc-secrets-engine-operator")
self.assertIs(ccr["openbao"]["auth"]["bound_claims_confirmed"], False)
def test_apply_plan_refuses_unapproved_ccr(self) -> None:
with self.assertRaises(SystemExit):
credential_change.command_apply_plan(type("Args", (), {"ref": str(self.issue_core)})())