Finalize proposed CCR-2026-0033 and its attended custody child (source only).
Correct the KV path into platform/workloads/, record secrets-engine consumer coordinates (D1-D4 pending), rotation ownership, live order and rollback, and add a decision-gated custody child with fake-only tests. Nothing executed. 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
01e0576132
commit
df24a77482
9 changed files with 538 additions and 10 deletions
|
|
@ -5,7 +5,7 @@ request_type: workload-kv-read
|
|||
title: KeyCape secrets-engine-openbao issuer workload reader
|
||||
status: proposed
|
||||
created: '2026-10-04'
|
||||
updated: '2026-10-04'
|
||||
updated: '2026-10-06'
|
||||
requester:
|
||||
agent: key-cape
|
||||
reason: KEY-WP-0037-T02 owner packet; proposed issuer-side read of separately approved
|
||||
|
|
@ -26,7 +26,7 @@ target:
|
|||
through sso-only ESO
|
||||
openbao:
|
||||
mount: platform
|
||||
kv_path: platform/services/keycape/secrets-engine-openbao
|
||||
kv_path: platform/workloads/keycape/secrets-engine-openbao
|
||||
fields:
|
||||
- CLIENT_SECRET
|
||||
policy_name: workload-kv-read-keycape-secrets-engine-openbao-eso
|
||||
|
|
@ -80,7 +80,7 @@ scaffold:
|
|||
draft_only: true
|
||||
consumer_repo: key-cape
|
||||
owner_repo: railiance-platform
|
||||
request_sha256: 380a5cbb6a5be887d77e7c9aa57eda18da8a7059568b2c5143e2555ed8ab9fdb
|
||||
request_sha256: ade2e10b0f65341051b955900c68931bdd56fd3a81bf452ae81229cc2390ad63
|
||||
catalog_request_sha256: null
|
||||
state_hub:
|
||||
workplan_id: RPF-WP-0035
|
||||
|
|
@ -91,4 +91,80 @@ delivery:
|
|||
target: sso/keycape-secrets-engine-openbao-client key client-secret; issuer-side
|
||||
delivery only
|
||||
review_bundle: docs/provisioning-windows/2026-10-04-1000/keycape
|
||||
consumer_file_delivery: not selected; requires secrets-engine source acceptance
|
||||
consumer_file_delivery: same-process attended custody child; no client-side OpenBao
|
||||
read
|
||||
boundary_decision:
|
||||
id: 632a8893-0d6a-4591-a50b-40bbdb798f32
|
||||
approves: custody boundary only (platform authoritative KV and least-privilege issuer
|
||||
delivery; secrets-engine-owned mode-0600 consumer file outside every worktree)
|
||||
does_not_approve: generation, KV write, fetch, delivery, registration or deployment
|
||||
path_correction: 'Candidate path platform/services/keycape/secrets-engine-openbao
|
||||
from the KeyCape packet is outside the platform-mount applier scope (WARDEN-WP-0028:
|
||||
platform/workloads/ only). Corrected to platform/workloads/keycape/secrets-engine-openbao,
|
||||
same field CLIENT_SECRET. Consumers bind to the sso Secret and the consumer file,
|
||||
not the KV path.'
|
||||
custody_child:
|
||||
script: scripts/keycape_secrets_engine_openbao_custody.py
|
||||
tests: tests/test_keycape_secrets_engine_openbao_custody.py
|
||||
status: source-only; never executed
|
||||
requires:
|
||||
- --approval-decision <founder decision id for this exact custody child>; refused
|
||||
when absent, when equal to the boundary decision, or not in the exact CCR review
|
||||
- CCR status approved with platform-owner, key-cape-owner and net-kingdom-owner
|
||||
approvals and bound_claims_confirmed true
|
||||
- attended warden platform-admin envelope; no BAO_TOKEN/VAULT_TOKEN in environment
|
||||
- umask 077, shell tracing off
|
||||
generation: one secrets.token_urlsafe(48) value in process memory (64 chars); never
|
||||
in argv, environment, stdout, stderr, logs, Git or the hub
|
||||
authoritative_write: KV-v2 write with options.cas=0 (absent) or cas=<observed current
|
||||
version> (rotation); value passed on bao stdin only; metadata-only preflight
|
||||
consumer_write: parent 0700 created or verified (owner = effective user); O_CREAT|O_EXCL|O_WRONLY
|
||||
temp file 0600 in the parent; write, fsync, rename, directory fsync; refused if
|
||||
the KV write did not succeed
|
||||
consumer_validation: secrets-engine service-auth check-secret-file --path <file>
|
||||
--json; only ok/check names recorded
|
||||
receipt: O_EXCL 0600 JSON with booleans, KV version, request id, CCR/decision ids,
|
||||
source digest and check-secret-file pass/fail only
|
||||
failure_cleanup: removes only its own exact temp file; a written KV version is retained
|
||||
and recorded (recovery is a fresh version-guarded run, never a KV read)
|
||||
retry: a prior failed receipt with kv_written and no consumer file permits one new
|
||||
version with cas=<that version>; no import or reuse of the earlier value
|
||||
consumer_copy:
|
||||
owner: secrets-engine (SECRETS-WP-0012, commit a3897c2)
|
||||
host: operator workstation (WSL2) - founder decision D1, recommended workstation
|
||||
only
|
||||
os_user: worsch - founder decision D2, recommended worsch
|
||||
file: /home/worsch/.secrets-engine/keycape/secrets-engine-openbao.client-secret
|
||||
mode: '0600 regular file, one token; parent /home/worsch/.secrets-engine/keycape
|
||||
0700 worsch'
|
||||
storage: persistent ext4 home - founder decision D3, recommended persistent and
|
||||
excluded from backup tools
|
||||
rotation_cadence: founder decision D4, recommended event-driven plus 90 days
|
||||
decisions_pending: SECRETS-WP-0012-T02 (needs_human)
|
||||
rotation_revocation:
|
||||
generation_and_authoritative_version: railiance-platform (custody child, version-guarded)
|
||||
issuer_pickup: key-cape / net-kingdom (ESO refresh then KeyCape restart)
|
||||
consumer_copy_replacement: secrets-engine file, executed inside platform child after
|
||||
issuer reload confirmed
|
||||
disable_on_compromise: key-cape / net-kingdom remove registration first
|
||||
retire_versions: railiance-platform per separate approval
|
||||
remove_consumer_copy: secrets-engine operator
|
||||
live_order:
|
||||
- 1 founder approves this CCR (status approved, three reviews, bound claims confirmed)
|
||||
and the exact custody-child decision; secrets-engine D1-D4 resolved
|
||||
- 2 attended custody child (this repo) writes KV v1 and consumer file; receipt
|
||||
- 3 platform applies issuer reader policy/role and sso-only ClusterSecretStore + ExternalSecret;
|
||||
sso Secret keycape-secrets-engine-openbao-client/client-secret exists (metadata
|
||||
receipt only)
|
||||
- 4 net-kingdom env patch secrets-engine-openbao-client.proposed.patch.yaml (NK-WP-0046)
|
||||
- 5 KeyCape registration merge (KEY-WP-0037-T03)
|
||||
- 6 KeyCape restart (rollout helper)
|
||||
- 7 KeyCape live issuance check (KEY-WP-0037-T04)
|
||||
- 8 platform OpenBao JWT role/policy secrets-engine-openbao and login proof (RPF-WP-0035-T02)
|
||||
rollback:
|
||||
before_step_4: delete ExternalSecret (uid precondition), detach role policies; consumer
|
||||
file removed by secrets-engine operator (exact file, then rmdir parent only if
|
||||
empty); KV version retained pending separate destroy approval
|
||||
after_step_4: net-kingdom rollout helper restores prior config/Deployment first,
|
||||
then the before_step_4 sequence
|
||||
cleanup_receipts: native, value-free, per step owner
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ target:
|
|||
through sso-only ESO
|
||||
openbao:
|
||||
mount: platform
|
||||
kv_path: platform/services/keycape/secrets-engine-openbao
|
||||
kv_path: platform/workloads/keycape/secrets-engine-openbao
|
||||
fields:
|
||||
- CLIENT_SECRET
|
||||
policy_name: workload-kv-read-keycape-secrets-engine-openbao-eso
|
||||
|
|
|
|||
|
|
@ -44,8 +44,14 @@ the refusal. This is separate from actual provider registration/custody.
|
|||
|
||||
## Implemented platform issuer-reader preparation
|
||||
|
||||
The KeyCape packet's candidate path is accepted here for proposed source review:
|
||||
`platform/services/keycape/secrets-engine-openbao`, field CLIENT_SECRET.
|
||||
The KeyCape packet's candidate path `platform/services/keycape/secrets-engine-openbao`
|
||||
is outside the platform-mount applier scope (`platform/workloads/` only) and was
|
||||
corrected on 2026-10-06 to `platform/workloads/keycape/secrets-engine-openbao`,
|
||||
field CLIENT_SECRET. Consumers bind to the sso Secret and the consumer file, not
|
||||
the KV path. The attended custody child
|
||||
`scripts/keycape_secrets_engine_openbao_custody.py` (source only, never run) and
|
||||
the secrets-engine consumer copy (SECRETS-WP-0012, a3897c2) are recorded in
|
||||
CCR-2026-0033 with the explicit live order.
|
||||
Proposed CCR-2026-0033 and input
|
||||
`docs/credential-lane-designs/keycape-secrets-engine-openbao-issuer-input.yaml`
|
||||
declare only the issuer reader. Policy
|
||||
|
|
|
|||
|
|
@ -50,5 +50,5 @@ spec:
|
|||
data:
|
||||
- secretKey: client-secret
|
||||
remoteRef:
|
||||
key: services/keycape/secrets-engine-openbao
|
||||
key: workloads/keycape/secrets-engine-openbao
|
||||
property: CLIENT_SECRET
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"status": "not-executed",
|
||||
"draft_only": true,
|
||||
"verified": false,
|
||||
"request_sha256": "380a5cbb6a5be887d77e7c9aa57eda18da8a7059568b2c5143e2555ed8ab9fdb",
|
||||
"request_sha256": "ade2e10b0f65341051b955900c68931bdd56fd3a81bf452ae81229cc2390ad63",
|
||||
"required_owner_checks": [
|
||||
"Exact revision/digest and required owner reviews",
|
||||
"Identity/auth claims confirmed separately; no inferred workload identity",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
path "platform/data/services/keycape/secrets-engine-openbao" {
|
||||
path "platform/data/workloads/keycape/secrets-engine-openbao" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
|
|
|||
223
scripts/keycape_secrets_engine_openbao_custody.py
Normal file
223
scripts/keycape_secrets_engine_openbao_custody.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
#!/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.
|
||||
|
||||
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).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import stat
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from state_hub_preflight_lane import ROOT, LaneError, bao, command, data # noqa: E402
|
||||
|
||||
CCR_ID = 'CCR-2026-0033'
|
||||
CCR_PATH = ROOT / 'credential-change-requests/CCR-2026-0033.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}$')
|
||||
|
||||
|
||||
def require(condition, reason):
|
||||
if not condition:
|
||||
raise LaneError(reason)
|
||||
|
||||
|
||||
def attended_envelope():
|
||||
return (Path.home().parent.name == '.warden-attended-login'
|
||||
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')
|
||||
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')
|
||||
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()
|
||||
|
||||
|
||||
def kv_current_version():
|
||||
"""Metadata only. 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']
|
||||
current = meta['current_version']
|
||||
version = meta['versions'][str(current)]
|
||||
require(not version.get('destroyed') and not version.get('deletion_time'), 'current_version_not_live')
|
||||
return current
|
||||
|
||||
|
||||
def write_kv(value, cas):
|
||||
result = data(bao(['write', '-format=json', KV_DATA, '-'],
|
||||
payload={'options': {'cas': cas}, 'data': {FIELD: value}}))
|
||||
require(result['data']['version'] == cas + 1, 'unexpected_kv_version')
|
||||
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 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 run(args, receipt):
|
||||
receipt['approval_decision_id'] = args.approval_decision
|
||||
receipt['ccr_sha256'] = 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
|
||||
else:
|
||||
require(current is None, 'existing_custody_requires_retry_receipt_or_rotation_approval')
|
||||
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['status'] = 'custody_written_pending_issuer_delivery'
|
||||
receipt['phase'] = 'done'
|
||||
|
||||
|
||||
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')
|
||||
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')
|
||||
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}
|
||||
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
try:
|
||||
run(args, receipt)
|
||||
except Exception as exc:
|
||||
receipt['status'] = 'failed'
|
||||
receipt['error'] = str(exc) if isinstance(exc, LaneError) else 'contained_operation_failed'
|
||||
finally:
|
||||
receipt['finished_at'] = datetime.now(timezone.utc).isoformat()
|
||||
with os.fdopen(fd, 'w') as out:
|
||||
json.dump(receipt, out, indent=2)
|
||||
out.write('\n')
|
||||
return 0 if receipt['status'] != 'failed' else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
194
tests/test_keycape_secrets_engine_openbao_custody.py
Normal file
194
tests/test_keycape_secrets_engine_openbao_custody.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""CCR-2026-0033 custody child: synthetic values and fakes only; no OpenBao, cluster or real file."""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'scripts'))
|
||||
import keycape_secrets_engine_openbao_custody as child # noqa: E402
|
||||
|
||||
DECISION = '00000000-0000-4000-8000-000000000033'
|
||||
SYNTHETIC = 'synthetic-test-value-' + 'x' * 43
|
||||
|
||||
|
||||
def result(code=0, stdout=b'', stderr=b''):
|
||||
return SimpleNamespace(returncode=code, stdout=stdout, stderr=stderr)
|
||||
|
||||
|
||||
class FakeBao:
|
||||
def __init__(self, current=None, fail_write=False):
|
||||
self.current, self.fail_write, self.calls, self.payloads = current, fail_write, [], []
|
||||
|
||||
def __call__(self, args, *, payload=None, token=None, allow_failure=False):
|
||||
self.calls.append(list(args))
|
||||
if args[:2] == ['token', 'lookup']:
|
||||
return result(stdout=json.dumps({'data': {'policies': ['default', 'platform-admin']}}).encode())
|
||||
if args[0] == 'read' and args[-1] == child.KV_METADATA:
|
||||
if self.current is None:
|
||||
return result(2, stderr=b'No value found at ' + child.KV_METADATA.encode())
|
||||
return result(stdout=json.dumps({'data': {'current_version': self.current,
|
||||
'versions': {str(self.current): {'destroyed': False, 'deletion_time': ''}}}}).encode())
|
||||
if args[0] == 'write' and args[2] == child.KV_DATA:
|
||||
self.payloads.append(payload)
|
||||
if self.fail_write or payload['options']['cas'] != (self.current or 0):
|
||||
raise child.LaneError('command_failed')
|
||||
self.current = (self.current or 0) + 1
|
||||
return result(stdout=json.dumps({'request_id': 'req-1', 'data': {'version': self.current}}).encode())
|
||||
raise AssertionError('unexpected bao call: ' + ' '.join(args[:2]))
|
||||
|
||||
|
||||
class CustodyChild(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
base = Path(self.tmp.name)
|
||||
os.chmod(base, 0o755)
|
||||
self.consumer = base / 'home/.secrets-engine/keycape/secrets-engine-openbao.client-secret'
|
||||
self.consumer.parent.parent.mkdir(parents=True)
|
||||
ccr = yaml.safe_load((ROOT / 'credential-change-requests/CCR-2026-0033.yaml').read_text())
|
||||
ccr['status'] = 'approved'
|
||||
ccr['openbao']['auth']['bound_claims_confirmed'] = True
|
||||
ccr['consumer_copy']['file'] = str(self.consumer)
|
||||
ccr['review']['comments'] = [
|
||||
{'reviewer': r, 'decision': 'approved', 'date': '2026-10-06',
|
||||
'comment': 'custody child approved by decision ' + DECISION} for r in child.APPROVERS]
|
||||
self.ccr = base / 'CCR-2026-0033.yaml'
|
||||
self.ccr.write_text(yaml.safe_dump(ccr, sort_keys=False))
|
||||
checker = base / 'check.py'
|
||||
checker.write_text('#!' + sys.executable + '\nimport json,os,stat,sys\np=sys.argv[sys.argv.index("--path")+1]\n'
|
||||
'ok=os.path.isfile(p) and stat.S_IMODE(os.stat(p).st_mode)==0o600\n'
|
||||
'print(json.dumps({"ok":ok}))\nsys.exit(0 if ok else 1)\n')
|
||||
checker.chmod(0o700)
|
||||
self.checker = str(checker)
|
||||
self.bao = FakeBao()
|
||||
self.patches = [patch.object(child, 'CCR_PATH', self.ccr), patch.object(child, 'CONSUMER_FILE', self.consumer),
|
||||
patch.object(child, 'bao', self.bao), patch.object(child, 'attended_envelope', lambda: True),
|
||||
patch.object(child.secrets, 'token_urlsafe', lambda n: SYNTHETIC)]
|
||||
for p in self.patches:
|
||||
p.start()
|
||||
self.umask = os.umask(0o022)
|
||||
|
||||
def tearDown(self):
|
||||
os.umask(self.umask)
|
||||
for p in reversed(self.patches):
|
||||
p.stop()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def run_child(self, *extra, decision=DECISION):
|
||||
receipt = Path(self.tmp.name) / ('receipt-%d.json' % len(list(Path(self.tmp.name).glob('receipt-*'))))
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
argv = ['provision', '--receipt', str(receipt), '--secrets-engine', self.checker, *extra]
|
||||
if decision is not None:
|
||||
argv += ['--approval-decision', decision]
|
||||
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||
code = child.main(argv)
|
||||
text = receipt.read_text()
|
||||
for blob in (text, out.getvalue(), err.getvalue(), json.dumps(self.bao.calls)):
|
||||
self.assertNotIn(SYNTHETIC, blob)
|
||||
self.assertEqual(oct(receipt.stat().st_mode & 0o777), oct(0o600))
|
||||
return code, json.loads(text), receipt
|
||||
|
||||
def test_refuses_without_or_with_wrong_decision(self):
|
||||
with self.assertRaises(SystemExit), contextlib.redirect_stderr(io.StringIO()):
|
||||
child.main(['provision', '--receipt', str(Path(self.tmp.name) / 'r.json')])
|
||||
for decision, reason in (('not-a-uuid', 'approval_decision_id_required'),
|
||||
(child.BOUNDARY_DECISION, 'boundary_decision_does_not_approve_custody_child'),
|
||||
('11111111-1111-4111-8111-111111111111', 'custody_child_decision_not_recorded_in_ccr')):
|
||||
code, receipt, _ = self.run_child(decision=decision)
|
||||
self.assertEqual((code, receipt['error']), (1, reason))
|
||||
self.assertEqual(self.bao.calls, [])
|
||||
|
||||
def test_committed_proposed_ccr_is_refused(self):
|
||||
with patch.object(child, 'CCR_PATH', ROOT / 'credential-change-requests/CCR-2026-0033.yaml'):
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual((code, receipt['error']), (1, 'approved_ccr_required'))
|
||||
self.assertEqual(self.bao.calls, [])
|
||||
|
||||
def test_refuses_outside_attended_envelope(self):
|
||||
with patch.object(child, 'attended_envelope', lambda: False):
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(receipt['error'], 'attended_warden_envelope_required')
|
||||
self.assertEqual(self.bao.calls, [])
|
||||
|
||||
def test_provision_writes_kv_and_consumer_in_one_process(self):
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(receipt['status'], 'custody_written_pending_issuer_delivery')
|
||||
self.assertEqual((receipt['kv_version'], receipt['kv_cas']), (1, 0))
|
||||
self.assertTrue(receipt['consumer_written'] and receipt['consumer_check_ok'])
|
||||
self.assertEqual(self.bao.payloads, [{'options': {'cas': 0}, 'data': {'CLIENT_SECRET': SYNTHETIC}}])
|
||||
self.assertNotIn(['read', '-format=json', child.KV_DATA], self.bao.calls)
|
||||
self.assertEqual(self.consumer.read_text(), SYNTHETIC + '\n')
|
||||
self.assertEqual(self.consumer.stat().st_mode & 0o777, 0o600)
|
||||
self.assertEqual(self.consumer.parent.stat().st_mode & 0o777, 0o700)
|
||||
self.assertEqual(sorted(p.name for p in self.consumer.parent.iterdir()), [self.consumer.name])
|
||||
|
||||
def test_existing_custody_or_consumer_file_is_refused(self):
|
||||
self.bao.current = 1
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(receipt['error'], 'existing_custody_requires_retry_receipt_or_rotation_approval')
|
||||
self.bao.current = None
|
||||
self.consumer.parent.mkdir(mode=0o700)
|
||||
self.consumer.write_text('old\n')
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(receipt['error'], 'consumer_file_exists_rotation_needs_separate_approval')
|
||||
self.assertEqual(self.bao.payloads, [])
|
||||
|
||||
def test_shared_parent_is_refused(self):
|
||||
self.consumer.parent.mkdir(mode=0o755)
|
||||
os.chmod(self.consumer.parent, 0o755)
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(receipt['error'], 'consumer_parent_must_be_owned_0700_directory')
|
||||
self.assertEqual(self.bao.payloads, [])
|
||||
|
||||
def test_failed_kv_write_writes_no_consumer_file(self):
|
||||
self.bao.fail_write = True
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual((code, receipt['kv_written'], receipt['consumer_written']), (1, False, False))
|
||||
self.assertFalse(self.consumer.exists())
|
||||
self.assertEqual(list(self.consumer.parent.iterdir()), [])
|
||||
|
||||
def test_consumer_failure_cleans_temp_and_retry_preserves_version(self):
|
||||
with patch.object(child.os, 'rename', side_effect=OSError('synthetic')):
|
||||
code, first, path = self.run_child()
|
||||
self.assertEqual((code, first['kv_written'], first['kv_version'], first['consumer_written']), (1, True, 1, False))
|
||||
self.assertTrue(first['consumer_temp_removed'])
|
||||
self.assertEqual(list(self.consumer.parent.iterdir()), [])
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual(receipt['error'], 'existing_custody_requires_retry_receipt_or_rotation_approval')
|
||||
code, second, _ = self.run_child('--prior-receipt', str(path))
|
||||
self.assertEqual((code, second['kv_version'], second['kv_cas'], second['retry_of_kv_version']), (0, 2, 1, 1))
|
||||
self.assertEqual([p['options']['cas'] for p in self.bao.payloads], [0, 1])
|
||||
self.bao.current = 3
|
||||
Path(self.consumer).unlink()
|
||||
code, third, _ = self.run_child('--prior-receipt', str(path))
|
||||
self.assertEqual(third['error'], 'kv_version_changed_since_prior_receipt')
|
||||
|
||||
def test_failed_consumer_check_fails_receipt(self):
|
||||
bad = Path(self.tmp.name) / 'bad.py'
|
||||
bad.write_text('#!' + sys.executable + '\nimport sys\nprint(\'{"ok": false}\')\nsys.exit(1)\n')
|
||||
bad.chmod(0o700)
|
||||
self.checker = str(bad)
|
||||
code, receipt, _ = self.run_child()
|
||||
self.assertEqual((code, receipt['consumer_written'], receipt['consumer_check_ok'], receipt['error']),
|
||||
(1, True, False, 'consumer_check_failed'))
|
||||
|
||||
def test_receipt_contains_only_metadata_keys(self):
|
||||
_, receipt, _ = self.run_child()
|
||||
self.assertTrue(set(receipt) <= {
|
||||
'schema', 'ccr', 'status', 'phase', 'started_at', 'finished_at', 'credential_values_emitted',
|
||||
'client_side_read_admitted', 'kv_written', 'consumer_written', 'approval_decision_id', 'ccr_sha256',
|
||||
'kv_version', 'kv_request_id', 'kv_cas', 'consumer_check_ok', 'retry_of_kv_version',
|
||||
'consumer_temp_removed', 'error'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -96,6 +96,16 @@ remains pending its scoped historical identity repair. Requirements and acceptan
|
|||
boundaries: `docs/provisioning-windows/2026-10-04-1000/dependency-requirements.md`.
|
||||
This workplan was already blocked and remains blocked on its open owner/live gates.
|
||||
|
||||
2026-10-06: secrets-engine returned consumer custody source acceptance
|
||||
(message 680136e2, SECRETS-WP-0012, commit a3897c2). CCR-2026-0033 finalized as
|
||||
**proposed**: KV path corrected to `platform/workloads/keycape/secrets-engine-openbao`
|
||||
(applier scope), consumer copy coordinates with D1-D4 pending founder choice,
|
||||
rotation/revocation ownership, live order and rollback. Attended custody child
|
||||
`scripts/keycape_secrets_engine_openbao_custody.py` with fake-only tests
|
||||
(`tests/test_keycape_secrets_engine_openbao_custody.py`) refuses without an exact
|
||||
founder decision id recorded in the approved CCR. Nothing executed, generated or
|
||||
applied; nothing added to active GitOps. The founder approval it needs is T09.
|
||||
|
||||
## Implement the platform operator-write CCR contract and Fluid lane
|
||||
|
||||
```task
|
||||
|
|
@ -751,3 +761,22 @@ neither source reconciliation nor the historical pilot is that evidence.
|
|||
## Loose-end review — 2026-09-27
|
||||
|
||||
T08 now explicitly waits for formerly-valid audit-bearer revocation acceptance from AUDIT-WP-0009-T09/T11. Native delivery, producer checks and independent readback already passed; do not reopen login, seed or review gates. T02/T03/T06/T07 retain their latest exact service registration, signed-in human acceptance, owner delivery/disposition and governed-consumer migration dependencies. No credential value was read or changed.
|
||||
|
||||
## Founder approval of the CCR-2026-0033 custody child
|
||||
|
||||
```task
|
||||
id: RPF-WP-0035-T09
|
||||
status: wait
|
||||
needs_human: true
|
||||
priority: high
|
||||
depends_on: [SECRETS-WP-0012-T02]
|
||||
blocking_reason: "Founder must approve proposed CCR-2026-0033 (platform-owner, key-cape-owner, net-kingdom-owner reviews; bound claims confirmed) and the exact attended custody child run with a decision id recorded in the CCR review. Boundary decision 632a8893 does not cover generation, write or delivery. secrets-engine D1-D4 resolve first."
|
||||
```
|
||||
|
||||
Packet: `credential-change-requests/CCR-2026-0033.yaml` (custody_child,
|
||||
consumer_copy, live_order, rollback). Run after approval, attended:
|
||||
`python3 scripts/keycape_secrets_engine_openbao_custody.py provision
|
||||
--approval-decision <id> --receipt <new path>` inside the warden platform-admin
|
||||
envelope. Later live steps (ESO, NetKingdom patch, registration, restart,
|
||||
KEY-WP-0037-T04 issuance, T02 JWT login proof) each need their own go.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue