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

@ -78,6 +78,9 @@ APPLIER_ALLOWED_ENVIRONMENTS = {
}
WORKLOAD_KV_POLICY_PREFIX = "workload-kv-read-"
OIDC_WORKLOAD_ROLE_SUFFIX = "-workload-kv-read"
TOKEN_TYPES = {"service", "batch", "default"}
TOKEN_BOUND_KEYS = ("token_max_ttl", "token_explicit_max_ttl", "token_no_default_policy",
"token_num_uses", "token_type")
KUBERNETES_ROLE_SUFFIXES = ("-workload-kv-read", "-secrets-read")
KUBERNETES_ROLE_PREFIXES = ("external-secrets-",)
EVIDENCE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
@ -248,6 +251,10 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
errors.append("openbao.metadata_read must be boolean")
if "token_self_lifecycle" in openbao and not isinstance(openbao["token_self_lifecycle"], bool):
errors.append("openbao.token_self_lifecycle must be boolean")
if "token_revoke_self" in openbao and not isinstance(openbao["token_revoke_self"], bool):
errors.append("openbao.token_revoke_self must be boolean")
if "kv_metadata" in openbao:
validate_kv_metadata(openbao["kv_metadata"], errors)
fields = [str(field) for field in require_list(openbao.get("fields"), "openbao.fields", errors)]
if not fields:
errors.append("openbao.fields must contain at least one field")
@ -276,14 +283,24 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
if method in {"oidc", "kubernetes"}:
require_string(auth.get("mount"), "openbao.auth.mount", errors)
require_string(auth.get("role"), "openbao.auth.role", errors)
if method == "kubernetes":
if "audience" in auth:
require_string(auth["audience"], "openbao.auth.audience", errors)
if method == "kubernetes" and "audience" in auth:
require_string(auth["audience"], "openbao.auth.audience", errors)
if method in {"oidc", "kubernetes"}:
for key in ("token_max_ttl", "token_explicit_max_ttl"):
if key in auth and (not isinstance(auth[key], str) or not TTL_RE.match(auth[key])):
errors.append(f"openbao.auth.{key} must be a TTL string")
if "token_no_default_policy" in auth and not isinstance(auth["token_no_default_policy"], bool):
errors.append("openbao.auth.token_no_default_policy must be boolean")
if "token_num_uses" in auth and (
isinstance(auth["token_num_uses"], bool)
or not isinstance(auth["token_num_uses"], int)
or auth["token_num_uses"] < 1
):
errors.append("openbao.auth.token_num_uses must be a positive integer")
if "token_type" in auth and auth["token_type"] not in TOKEN_TYPES:
errors.append(f"openbao.auth.token_type must be one of {sorted(TOKEN_TYPES)}")
if method == "oidc" and "bound_claims_type" in auth and auth["bound_claims_type"] != "string":
errors.append("openbao.auth.bound_claims_type must be 'string'; glob matching is not admitted")
if method == "oidc":
redirect_uris = require_list(
auth.get("allowed_redirect_uris"),
@ -432,6 +449,33 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
require_string(lifecycle.get(field), f"lifecycle.{field}", errors)
def validate_kv_metadata(value: Any, errors: list[str]) -> None:
"""Declared KV-v2 metadata, applied by the owning custody child, never by the applier."""
meta = require_object(value, "openbao.kv_metadata", errors)
if not meta:
return
unknown = set(meta) - {"cas_required", "max_versions", "delete_version_after", "custom_metadata"}
if unknown:
errors.append(f"openbao.kv_metadata has unknown keys: {sorted(unknown)}")
if "cas_required" in meta and not isinstance(meta["cas_required"], bool):
errors.append("openbao.kv_metadata.cas_required must be boolean")
max_versions = meta.get("max_versions")
if max_versions is not None and (
isinstance(max_versions, bool) or not isinstance(max_versions, int) or max_versions < 1
):
errors.append("openbao.kv_metadata.max_versions must be a positive integer")
after = meta.get("delete_version_after")
if after is not None and after != "0s" and (not isinstance(after, str) or not TTL_RE.match(after)):
errors.append("openbao.kv_metadata.delete_version_after must be '0s' or a TTL string")
custom = meta.get("custom_metadata")
if custom is not None:
custom = require_object(custom, "openbao.kv_metadata.custom_metadata", errors)
for key, item in custom.items():
if not isinstance(key, str) or not isinstance(item, str) or not key or not item:
errors.append("openbao.kv_metadata.custom_metadata must map non-empty strings to strings")
break
def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
errors: list[str] = []
warnings: list[str] = []
@ -573,6 +617,8 @@ def generated_policy_hcl(ccr: dict[str, Any]) -> str:
if openbao.get("token_self_lifecycle", False):
body += ('\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n'
'\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n')
elif openbao.get("token_revoke_self", False):
body += '\npath "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'
return body
@ -641,7 +687,7 @@ def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]:
"policies": ",".join(auth["policies"]),
"ttl": auth.get("ttl", "15m"),
}
for key in ("audience", "token_max_ttl", "token_explicit_max_ttl", "token_no_default_policy"):
for key in ("audience", *TOKEN_BOUND_KEYS):
if key in auth:
payload[key] = auth[key]
return payload
@ -660,6 +706,9 @@ def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]:
payload["allowed_redirect_uris"] = auth["allowed_redirect_uris"]
if auth.get("oidc_scopes"):
payload["oidc_scopes"] = auth["oidc_scopes"]
for key in ("bound_claims_type", *TOKEN_BOUND_KEYS):
if key in auth:
payload[key] = auth[key]
return payload

View file

@ -1,14 +1,16 @@
#!/usr/bin/env python3
"""Attended custody child for CCR-2026-0033: KeyCape `secrets-engine-openbao` client secret.
SOURCE ONLY until the founder approves this exact child. One run generates one
value in process memory, compare-before-writes it as the authoritative KV-v2
version, writes the secrets-engine consumer copy in the same process, runs the
consumer's value-free acceptance check and writes a metadata-only receipt.
SOURCE ONLY until the founder approves this exact child (A3). One run generates
one value in process memory, writes it as the authoritative KV-v2 version with
compare-and-set (cas=0 to create, cas=<current> to rotate with --rotate), then
compare-before-writes the declared KV metadata and writes a metadata-only receipt.
It never reads the KV value, never prints the value, never puts it in argv or
the environment, and does not touch policies, roles, ESO, KeyCape or the
cluster (later live steps, see CCR-2026-0033 live_order).
There is no consumer copy: secrets-engine reads the value in memory per command
through the attended operator reader CCR-2026-0034 (founder direction 2026-10-06,
ops-mason plan keycape-secrets-engine-openbao-client-custody). The child never
reads the KV value, never prints it, never puts it in argv or the environment,
and does not touch policies, roles, ESO, KeyCape or the cluster.
"""
from __future__ import annotations
@ -20,7 +22,6 @@ import os
from pathlib import Path
import re
import secrets
import stat
import sys
from datetime import datetime, timezone
@ -29,13 +30,13 @@ from state_hub_preflight_lane import ROOT, LaneError, bao, command, data # noqa
CCR_ID = 'CCR-2026-0033'
CCR_PATH = ROOT / 'credential-change-requests/CCR-2026-0033.yaml'
SIBLING_ID = 'CCR-2026-0034'
SIBLING_PATH = ROOT / 'credential-change-requests/CCR-2026-0034.yaml'
BOUNDARY_DECISION = '632a8893-0d6a-4591-a50b-40bbdb798f32'
KV_PATH = 'platform/workloads/keycape/secrets-engine-openbao'
KV_DATA = 'platform/data/workloads/keycape/secrets-engine-openbao'
KV_METADATA = 'platform/metadata/workloads/keycape/secrets-engine-openbao'
FIELD = 'CLIENT_SECRET'
# secrets-engine coordinates (SECRETS-WP-0012, a3897c2); D1-D4 pending founder choice.
CONSUMER_FILE = Path('/home/worsch/.secrets-engine/keycape/secrets-engine-openbao.client-secret')
APPROVERS = ('platform-owner', 'key-cape-owner', 'net-kingdom-owner')
UUID_RE = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
@ -50,36 +51,54 @@ def attended_envelope():
and not os.environ.get('BAO_TOKEN') and not os.environ.get('VAULT_TOKEN'))
def contract(decision_id):
require(isinstance(decision_id, str) and UUID_RE.match(decision_id), 'approval_decision_id_required')
require(decision_id != BOUNDARY_DECISION, 'boundary_decision_does_not_approve_custody_child')
def _load_validator():
spec = importlib.util.spec_from_file_location('credential_change', ROOT / 'scripts/credential-change.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
ccr, errors, _ = module.validate_ccr(CCR_PATH)
require(not errors and ccr['id'] == CCR_ID, 'valid_ccr_required')
return module
def _approved(module, path, ccr_id):
ccr, errors, _ = module.validate_ccr(path)
require(not errors and ccr['id'] == ccr_id, 'valid_ccr_required')
require(ccr['status'] == 'approved', 'approved_ccr_required')
comments = ccr['review']['comments']
for role in APPROVERS:
require(any(c.get('decision') == 'approved' and role in c.get('reviewer', '') for c in comments),
'all_named_reviews_required')
require(any(c.get('decision') == 'approved' and decision_id in str(c.get('comment', '')) for c in comments),
'custody_child_decision_not_recorded_in_ccr')
openbao = ccr['openbao']
require(openbao['kv_path'] == KV_PATH and openbao['fields'] == [FIELD]
and openbao['auth']['bound_claims_confirmed'] is True
and ccr['custody_child']['script'] == 'scripts/keycape_secrets_engine_openbao_custody.py'
and ccr['consumer_copy']['file'] == str(CONSUMER_FILE), 'exact_approved_contract_required')
return hashlib.sha256(CCR_PATH.read_bytes()).hexdigest()
require(any(c.get('decision') == 'approved' and role in c.get('reviewer', '')
for c in ccr['review']['comments']), 'all_named_reviews_required')
require(ccr['openbao']['auth']['bound_claims_confirmed'] is True, 'bound_claims_confirmation_required')
require(ccr['openbao']['kv_path'] == KV_PATH and ccr['openbao']['fields'] == [FIELD],
'exact_approved_contract_required')
return ccr
def kv_current_version():
"""Metadata only. None when the path has never been written."""
def contract(decision_id):
"""Returns (ccr digest, desired metadata). Refuses before any OpenBao call."""
require(isinstance(decision_id, str) and UUID_RE.match(decision_id), 'approval_decision_id_required')
require(decision_id != BOUNDARY_DECISION, 'boundary_decision_does_not_approve_custody_child')
module = _load_validator()
ccr = _approved(module, CCR_PATH, CCR_ID)
require(any(c.get('decision') == 'approved' and decision_id in str(c.get('comment', ''))
for c in ccr['review']['comments']), 'custody_child_decision_not_recorded_in_ccr')
require(ccr['custody_child']['script'] == 'scripts/keycape_secrets_engine_openbao_custody.py'
and 'consumer_copy' not in ccr, 'exact_approved_contract_required')
_approved(module, SIBLING_PATH, SIBLING_ID)
meta = ccr['openbao'].get('kv_metadata') or {}
require(meta.get('cas_required') is True and meta.get('max_versions') == 2
and meta.get('delete_version_after') == '0s' and meta.get('custom_metadata'),
'declared_kv_metadata_required')
return hashlib.sha256(CCR_PATH.read_bytes()).hexdigest(), meta
def read_metadata():
"""Metadata only (never the data path). None when the path has never been written."""
result = bao(['read', '-format=json', KV_METADATA], allow_failure=True)
if result.returncode != 0:
require(b'404' in result.stderr or b'No value found' in result.stderr, 'absence_not_proven')
return None
meta = data(result)['data']
return data(result)['data']
def live_version(meta):
current = meta['current_version']
version = meta['versions'][str(current)]
require(not version.get('destroyed') and not version.get('deletion_time'), 'current_version_not_live')
@ -93,100 +112,46 @@ def write_kv(value, cas):
return result['data']['version'], result.get('request_id')
def fsync_dir(path):
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
def metadata_matches(observed, desired):
observed = observed or {}
return all((observed.get(key) or {}) == value if key == 'custom_metadata' else observed.get(key) == value
for key, value in desired.items())
def prepare_parent(target):
parent = target.parent
grand = os.lstat(parent.parent)
require(stat.S_ISDIR(grand.st_mode), 'consumer_grandparent_invalid')
try:
info = os.lstat(parent)
except FileNotFoundError:
os.mkdir(parent, 0o700)
fsync_dir(parent.parent)
info = os.lstat(parent)
require(stat.S_ISDIR(info.st_mode) and info.st_uid == os.geteuid()
and stat.S_IMODE(info.st_mode) == 0o700, 'consumer_parent_must_be_owned_0700_directory')
require(not os.path.lexists(target), 'consumer_file_exists_rotation_needs_separate_approval')
def write_consumer(target, value, receipt):
parent = target.parent
temp = parent / ('.' + target.name + '.' + secrets.token_hex(8) + '.tmp')
fd = os.open(temp, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
try:
try:
os.write(fd, (value + '\n').encode())
os.fsync(fd)
finally:
os.close(fd)
os.rename(temp, target)
fsync_dir(parent)
except BaseException:
try:
os.unlink(temp)
receipt['consumer_temp_removed'] = True
except FileNotFoundError:
pass
raise
info = os.lstat(target)
require(stat.S_ISREG(info.st_mode) and stat.S_IMODE(info.st_mode) == 0o600
and info.st_uid == os.geteuid(), 'consumer_file_readback_failed')
def check_consumer(target, secrets_engine):
result = command([secrets_engine, 'service-auth', 'check-secret-file', '--path', str(target), '--json'],
allow_failure=True)
try:
ok = bool(json.loads(result.stdout).get('ok'))
except (ValueError, AttributeError):
ok = False
return result.returncode == 0 and ok
def retry_cas(prior_path, current):
prior = json.loads(Path(prior_path).read_text())
require(prior.get('ccr') == CCR_ID and prior.get('status') == 'failed' and prior.get('kv_written')
and not prior.get('consumer_written'), 'retryable_partial_receipt_required')
require(current is not None and prior.get('kv_version') == current, 'kv_version_changed_since_prior_receipt')
return current
def ensure_metadata(desired):
"""Compare before write; idempotent. Returns 'unchanged' or 'applied'."""
if metadata_matches(read_metadata(), desired):
return 'unchanged'
bao(['write', KV_METADATA, '-'], payload=dict(desired))
require(metadata_matches(read_metadata(), desired), 'kv_metadata_readback_mismatch')
return 'applied'
def run(args, receipt):
receipt['approval_decision_id'] = args.approval_decision
receipt['ccr_sha256'] = contract(args.approval_decision)
receipt['ccr_sha256'], desired = contract(args.approval_decision)
require(attended_envelope(), 'attended_warden_envelope_required')
os.umask(0o077)
identity = data(bao(['token', 'lookup', '-format=json']))['data']
require('platform-admin' in identity['policies'] and 'root' not in identity['policies'],
'attended_platform_admin_required')
current = kv_current_version()
if args.prior_receipt:
cas = retry_cas(args.prior_receipt, current)
receipt['retry_of_kv_version'] = cas
meta = read_metadata()
current = None if meta is None else live_version(meta)
if args.rotate:
require(current is not None, 'rotation_requires_existing_custody')
cas = current
else:
require(current is None, 'existing_custody_requires_retry_receipt_or_rotation_approval')
require(current is None, 'existing_custody_requires_rotate')
cas = 0
prepare_parent(CONSUMER_FILE)
receipt['phase'] = 'kv_write'
value = secrets.token_urlsafe(48)
try:
version, request_id = write_kv(value, cas)
receipt.update(kv_written=True, kv_version=version, kv_request_id=request_id, kv_cas=cas)
receipt['phase'] = 'consumer_write'
write_consumer(CONSUMER_FILE, value, receipt)
receipt['consumer_written'] = True
finally:
value = None
receipt['phase'] = 'consumer_check'
receipt['consumer_check_ok'] = check_consumer(CONSUMER_FILE, args.secrets_engine)
require(receipt['consumer_check_ok'], 'consumer_check_failed')
receipt.update(kv_written=True, kv_version=version, kv_request_id=request_id, kv_cas=cas)
receipt['phase'] = 'kv_metadata'
receipt['kv_metadata'] = ensure_metadata(desired)
receipt['status'] = 'custody_written_pending_issuer_delivery'
receipt['phase'] = 'done'
@ -195,16 +160,16 @@ def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument('action', choices=['provision'])
parser.add_argument('--approval-decision', required=True,
help='founder decision id approving this exact custody child')
help='founder decision id approving this exact custody child (A3)')
parser.add_argument('--receipt', required=True)
parser.add_argument('--prior-receipt', help='failed receipt with kv_written and no consumer file')
parser.add_argument('--secrets-engine', default='secrets-engine', help='secrets-engine executable')
parser.add_argument('--rotate', action='store_true',
help='roll forward an existing entry with cas=<observed current version>')
args = parser.parse_args(argv)
receipt = {'schema': 'platform.keycape-secrets-engine-openbao-custody.v1', 'ccr': CCR_ID,
'status': 'failed', 'phase': 'preflight',
'started_at': datetime.now(timezone.utc).isoformat(),
'credential_values_emitted': False, 'client_side_read_admitted': False,
'kv_written': False, 'consumer_written': False}
'kv_written': False}
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
run(args, receipt)