Revise KeyCape secrets-engine-openbao custody: OpenBao-only, operator reader CCR-2026-0034 (source only)
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:
parent
9e23eaa067
commit
4a284f0a3b
11 changed files with 734 additions and 307 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue