From 4b34c239bc762664b7992f07e4467841b09e9062 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 15 Sep 2026 20:08:02 +0200 Subject: [PATCH] Approve sitting-requester CCRs and add the attended provisioner. CCR-2026-0026/0027 are approved for the create-only informed-decision client. Live KeyCape registration and CAS=0 custody stay in the contained helper; no sitting POST. Assistant: grok Assistant-Session: 01a0a23b-3bf0-7341-b4e5-9dc05f72573a --- ...d-decision-sitting-requester-verifier.yaml | 7 +- ...med-decision-sitting-requester-reader.yaml | 7 +- scripts/provision-sitting-requester.py | 101 ++++++++++++++++++ scripts/provision-sitting-requester.sh | 4 + tests/test_sitting_requester_provisioning.py | 38 +++++++ workplans/README.md | 2 +- ...042-informed-decision-sitting-requester.md | 12 +-- 7 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 scripts/provision-sitting-requester.py create mode 100755 scripts/provision-sitting-requester.sh create mode 100644 tests/test_sitting_requester_provisioning.py diff --git a/credential-change-requests/CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml b/credential-change-requests/CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml index 8006eec..46d6512 100644 --- a/credential-change-requests/CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml +++ b/credential-change-requests/CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml @@ -3,7 +3,7 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: Informed Decision sitting-requester KeyCape verifier custody -status: proposed +status: approved created: '2026-09-15' updated: '2026-09-15' requester: @@ -23,6 +23,11 @@ review: comment: Operator selected allocation of the sitting-requester CCR pair. Source only. No OpenBao apply, no secret seed, no KeyCape registration, and no sitting POST from this allocation. + - at: '2026-09-15' + reviewer: User (platform-operator; key-cape-owner) + decision: approved + comment: User instructed register sitting-requester. Apply remains the attended + helper with CAS=0 custody, exact create-only client, and no sitting POST. target: domain: financials tenant: platform diff --git a/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml b/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml index e0a2b9a..b112e4b 100644 --- a/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml +++ b/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml @@ -3,7 +3,7 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: Informed Decision sitting-requester attended operator reader -status: proposed +status: approved created: '2026-09-15' updated: '2026-09-15' requester: @@ -22,6 +22,11 @@ review: decision: allocated comment: Operator selected allocation of the sitting-requester CCR pair. Source only. No OpenBao apply, no secret seed, and no sitting POST from this allocation. + - at: '2026-09-15' + reviewer: User (platform-operator; key-cape-owner) + decision: approved + comment: User instructed register sitting-requester. Apply remains the attended + helper with CAS=0 custody, exact create-only client, and no sitting POST. target: domain: financials tenant: platform diff --git a/scripts/provision-sitting-requester.py b/scripts/provision-sitting-requester.py new file mode 100644 index 0000000..079d89d --- /dev/null +++ b/scripts/provision-sitting-requester.py @@ -0,0 +1,101 @@ +"""Silent attended provisioning of the informed-decision sitting requester.""" +import copy, json, os, secrets, sys, time +from pathlib import Path +from datetime import datetime, timezone +ROOT = Path('/home/worsch/railiance-platform') +sys.path.insert(0, str(ROOT / 'scripts')) +from state_hub_preflight_lane import command, bao, data, assert_cluster, LaneError +from keycape_approval_custody import require, read_optional, BOUNDARY +import importlib.util +KUBE = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-railiance01'] +KV = 'platform/data/workloads/informed-decision/sitting-requester' +META = 'platform/metadata/workloads/informed-decision/sitting-requester' +CLIENT = json.loads(Path('/home/worsch/key-cape/docs/sitting-requester-registration.json').read_text()) +RECEIPT = ROOT / 'docs/evidence/2026-09-15-sitting-requester-provision.json' +CCRS = ('CCR-2026-0026', 'CCR-2026-0027') +ENV_NAME = 'KEYCAPE_INFORMED_DECISION_SITTING_REQUESTER_CLIENT_SECRET' +ESO = ROOT / 'argocd/platform-addons/openbao-secretstore/sitting-requester.yaml' + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +rollout = load('requester_rollout', '/home/worsch/net-kingdom/sso-mfa/k8s/keycape/approval-clients-rollout.py') +rollout.IDS = ('informed-decision-sitting-requester',) + + +def contract(): + require(CLIENT['clientId'] == 'informed-decision-sitting-requester', 'requester_contract_drift') + require(CLIENT['allowedScopes'] == ['approval:create'], 'requester_contract_drift') + require(CLIENT['serviceSubject'] == 'informed-decision', 'requester_contract_drift') + require(CLIENT['audience'] == 'approval-engine', 'requester_contract_drift') + require(CLIENT['tenant'] == 'tenant:platform', 'requester_contract_drift') + require('approval:consume' not in CLIENT['allowedScopes'], 'consume_scope_forbidden') + require('approval:approve' not in CLIENT['allowedScopes'], 'approve_scope_forbidden') + + +def run(receipt): + require(Path.home().parent.name == '.warden-attended-login' and not os.getenv('BAO_TOKEN') and not os.getenv('VAULT_TOKEN'), 'attended_envelope_required') + assert_cluster(KUBE) + identity = data(bao(['token', 'lookup', '-format=json']))['data'] + require('platform-admin' in identity['policies'] and 'root' not in identity['policies'], 'attended_operator_required') + contract() + require(read_optional(META) is None, 'existing_sitting_requester_custody_requires_reconciliation') + for ref in CCRS: + command(['python3', str(ROOT / 'scripts/credential-change.py'), 'applier-dry-run', ref, '--json']) + secret = rollout.get(KUBE, 'secret', 'keycape-config') + deployment = rollout.get(KUBE, 'deployment', 'keycape') + config = rollout.replacement(secret, [CLIENT]) + spec = copy.deepcopy(deployment['spec']) + require(spec.get('replicas', 1) == 1 and spec['strategy']['type'] == 'Recreate', 'single_recreate_keycape_required') + container = next(c for c in spec['template']['spec']['containers'] if c['name'] == 'keycape') + image = container['image'] + require('@sha256:' in image, 'pinned_keycape_image_required') + require(not any(e['name'] == ENV_NAME for e in container.setdefault('env', [])), 'existing_sitting_requester_env') + container['env'].append({'name': ENV_NAME, 'valueFrom': {'secretKeyRef': {'name': 'keycape-informed-decision-sitting-requester-client', 'key': 'client-secret'}}}) + rollout.patch_object(KUBE, 'secret', secret, '/data/config.yaml', config, dry=True) + rollout.patch_object(KUBE, 'deployment', deployment, '/spec', spec, dry=True) + receipt['phase'] = 'metadata_preflight_passed' + boundary = read_optional(BOUNDARY) + require(boundary is not None, 'agent_boundary_required') + additions = ''.join('path "' + p + '" { capabilities = ["deny"] }\n' for p in (KV, META)) + require(all('"' + p + '"' not in boundary['policy'] for p in (KV, META)), 'boundary_path_requires_reconciliation') + bao(['write', BOUNDARY, '-'], payload={'policy': boundary['policy'] + '\n' + additions}) + require(read_optional(BOUNDARY)['policy'] == boundary['policy'] + '\n' + additions, 'boundary_readback_failed') + for ref in CCRS: + command(['python3', str(ROOT / 'scripts/credential-change.py'), 'applier-apply', ref, '--actor', 'operator via attended sitting-requester custody session', '--confirm', 'DELEGATED APPLY ' + ref, '--quiet']) + receipt['phase'] = 'reader_roles_applied' + result = data(bao(['write', '-format=json', KV, '-'], payload={'options': {'cas': 0}, 'data': {'CLIENT_SECRET': secrets.token_urlsafe(48)}})) + require(result['data']['version'] == 1, 'initial_version_mismatch') + receipt.update(phase='custody_seeded', kv_version=1, request_id=result.get('request_id')) + RECEIPT.write_text(json.dumps(receipt, indent=2) + '\n') + command(KUBE + ['apply', '-f', str(ESO)]) + deadline = time.monotonic() + 90 + while time.monotonic() < deadline: + obj = rollout.get(KUBE, 'externalsecret', 'keycape-informed-decision-sitting-requester-client') + if any(c.get('type') == 'Ready' and c.get('status') == 'True' for c in obj.get('status', {}).get('conditions', [])): + break + time.sleep(2) + else: + raise LaneError('verifier_sync_failed') + receipt['phase'] = 'verifier_synced' + rollout.patch_object(KUBE, 'secret', secret, '/data/config.yaml', config) + after = rollout.get(KUBE, 'secret', 'keycape-config') + require(after['data'] == dict(secret['data'], **{'config.yaml': config}), 'unrelated_config_or_key_changed') + rollout.patch_object(KUBE, 'deployment', deployment, '/spec', spec) + receipt['keycape'] = rollout.ready(KUBE, image) + receipt.update(status='applied', phase='keycape_ready', image_unchanged=image, unrelated_config_preserved=True, verifier_secret_exported=False, sitting_post=False) + + +if __name__ == '__main__': + receipt = {'observed_at': datetime.now(timezone.utc).isoformat(), 'status': 'failed', 'phase': 'preflight', 'ccrs': list(CCRS), 'credential_values_emitted': False, 'approval_mutations': False, 'sitting_post': False} + try: + run(receipt) + except Exception: + raise SystemExit(1) from None + finally: + RECEIPT.write_text(json.dumps(receipt, indent=2) + '\n') diff --git a/scripts/provision-sitting-requester.sh b/scripts/provision-sitting-requester.sh new file mode 100755 index 0000000..838dd42 --- /dev/null +++ b/scripts/provision-sitting-requester.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Silent child for the governed attended login; no secret output. +set -euo pipefail +exec python3 "$(dirname "$0")/provision-sitting-requester.py" "$@" >/dev/null 2>&1 diff --git a/tests/test_sitting_requester_provisioning.py b/tests/test_sitting_requester_provisioning.py new file mode 100644 index 0000000..d605a82 --- /dev/null +++ b/tests/test_sitting_requester_provisioning.py @@ -0,0 +1,38 @@ +import importlib.util +from pathlib import Path +import unittest + +spec = importlib.util.spec_from_file_location( + 'sitting', Path(__file__).resolve().parents[1] / 'scripts/provision-sitting-requester.py') +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) + + +class SittingRequesterProvisioningTests(unittest.TestCase): + def test_contract_is_create_only_informed_decision(self): + m.contract() + self.assertEqual(m.CLIENT['clientId'], 'informed-decision-sitting-requester') + self.assertEqual(m.CLIENT['allowedScopes'], ['approval:create']) + self.assertEqual(m.CLIENT['serviceSubject'], 'informed-decision') + self.assertEqual(m.CCRS, ('CCR-2026-0026', 'CCR-2026-0027')) + self.assertNotIn('CCR-2026-0024', m.CCRS) + self.assertNotIn('CCR-2026-0025', m.CCRS) + + def test_contract_refuses_widened_scopes(self): + original = list(m.CLIENT['allowedScopes']) + m.CLIENT['allowedScopes'] = ['approval:create', 'approval:consume'] + with self.assertRaises(Exception): + m.contract() + m.CLIENT['allowedScopes'] = original + + def test_source_keycape_registration_matches_helper(self): + import yaml + clients = yaml.safe_load(Path('/home/worsch/key-cape/config/service-clients.example.yaml').read_text())['clients'] + named = next(c for c in clients if c['clientId'] == 'informed-decision-sitting-requester') + self.assertEqual(named['secretRef'], 'env:' + m.ENV_NAME) + self.assertEqual(named['allowedScopes'], ['approval:create']) + self.assertEqual(named['serviceSubject'], 'informed-decision') + + +if __name__ == '__main__': + unittest.main() diff --git a/workplans/README.md b/workplans/README.md index e223bce..29e8894 100644 --- a/workplans/README.md +++ b/workplans/README.md @@ -14,7 +14,7 @@ implementations or independent incidents. | [RPF-WP-0035](RPF-WP-0035-credential-lane-implementation.md) | Three remaining lanes: secrets-engine JWT, Fluid operator KV, KeyCape approval clients | Signing T04 is complete; T05 admission answered and awaiting owner approval plus a founder-attended window. | | [RPF-WP-0036](RPF-WP-0036-platform-service-assurance.md) | Implemented local assurance/admission; waits for recurring restore evidence, Q2 reception and owner handoff | Run the assurance commands; live acceptance and external ownership remain gated. | | [RPF-WP-0038](RPF-WP-0038-forgejo-scaleway-primary-coverage.md) | Native backup, full Scaleway archive recovery and 273 MiB Nextcloud essentials recovery verified; scheduled tier cutover remains | Bind recurring caller/dependencies, verified inventory, quota checks and separate owner retention. | -| [RPF-WP-0042](RPF-WP-0042-informed-decision-sitting-requester.md) | Sitting-requester CCR-2026-0026/0027 allocated; no apply | Do not widen 0024/0025. Attended seed waits on owner reviews and KeyCape row. | +| [RPF-WP-0042](RPF-WP-0042-informed-decision-sitting-requester.md) | Sitting-requester CCRs approved; live KeyCape/OpenBao apply remains | Attended `provision-sitting-requester.sh`; no sitting POST. | RPF-WP-0036-T02/T05/T07 are complete; T03/T04/T06 retain the remaining acceptance gates. Treat credential exposure closure as the highest-priority attended diff --git a/workplans/RPF-WP-0042-informed-decision-sitting-requester.md b/workplans/RPF-WP-0042-informed-decision-sitting-requester.md index 845432e..bf89d4e 100644 --- a/workplans/RPF-WP-0042-informed-decision-sitting-requester.md +++ b/workplans/RPF-WP-0042-informed-decision-sitting-requester.md @@ -4,7 +4,7 @@ type: workplan title: "Allocate Informed Decision sitting-requester custody" domain: financials repo: railiance-platform -status: ready +status: active flavor: implementation owner: grok topic_slug: railiance @@ -38,12 +38,12 @@ non-resolvable. ESO projection is unapplied source. ```task id: RPF-WP-0042-T02 -status: wait +status: progress priority: high state_hub_task_id: "c6fbf99c-de2b-55be-9f0c-58be9fe7c518" ``` -Requires named owner reviews, KeyCape row `informed-decision-sitting-requester`, -attended CAS=0 custody, exact policy/auth readback, sibling -`secrets-engine/approval-requester` denial, and create-only token-exchange proof. -No sitting POST until that proof exists. +Operator approved CCR-2026-0026/0027 on 2026-09-15. Source registration is in +`key-cape/config/service-clients.example.yaml`. Live apply is the silent helper +`scripts/provision-sitting-requester.sh` through `openbao-attended-exec.py`. +No sitting POST until exchange proof exists. Do not widen CCR-2026-0024/0025.