diff --git a/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml b/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml index 4577092..40017fb 100644 --- a/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml +++ b/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml @@ -3,9 +3,9 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: KeyCape verifier custody for the secrets-engine-approval confidential client -status: approved +status: verified created: '2026-09-08' -updated: '2026-09-08' +updated: '2026-09-09' requester: agent: claude reason: "KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260)\ @@ -140,7 +140,29 @@ verification: - Policy, Kubernetes auth role and ClusterSecretStore applied before the ExternalSecret; sync confirmed before the KeyCape image is rolled out. - Positive and negative results recorded with non-secret request ids or timestamps. - evidence: [] + evidence: + - at: '2026-09-09T00:14:27+00:00' + actor: the-custodian (codex) + kind: verifier_custody_and_rollout + result: passed + details: + - Named user approval recorded for platform-operator and key-cape-owner on 2026-09-09. + - Initial CAS=0 custody created version 1. Verified rollback/resume retained the + same values and versions. + - Both stores Valid and ExternalSecrets SecretSynced; exact native read, sibling/listing + and wrong service account/namespace/store-use denials passed; reader revocation + and coding-agent deny precedence passed. + - Pinned image 7ff54c54e63e has one ready replica; protected previous config/image + retained and signing key unchanged. + - Both clients passed live JWKS and exact audience/subject/tenant/role/scope/900-second + lifetime checks, excess-scope and wrong-secret denials; human client consume + denied. + - Native verifier from pinned image ran in the attended owner process with memory-only + credentials. Pod network policy remains unchanged. Future iat bound is the existing + 30-second contract; expiry has zero leeway. + - Fresh post-rollout OpenBao OIDC login succeeded and self-revoked. No client-side + fetch front door or factory spending was admitted. + - 'Receipt: docs/evidence/2026-09-09-keycape-verifier-admission.json' lifecycle: deactivate: KeyCape disables the secrets-engine-approval registration; platform detaches the policy from role external-secrets-keycape-secrets-engine-approval diff --git a/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml b/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml index 8692580..0c2b0d8 100644 --- a/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml +++ b/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml @@ -3,9 +3,9 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: KeyCape verifier custody for the approval-engine-operator confidential client -status: approved +status: verified created: '2026-09-08' -updated: '2026-09-08' +updated: '2026-09-09' requester: agent: claude reason: 'Second of the two registrations in KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260): @@ -122,7 +122,29 @@ verification: - Policy, Kubernetes auth role and ClusterSecretStore applied before the ExternalSecret; sync confirmed before the KeyCape rollout. - Positive and negative results recorded with non-secret request ids or timestamps. - evidence: [] + evidence: + - at: '2026-09-09T00:14:27+00:00' + actor: the-custodian (codex) + kind: verifier_custody_and_rollout + result: passed + details: + - Named user approval recorded for platform-operator and key-cape-owner on 2026-09-09. + - Initial CAS=0 custody created version 1. Verified rollback/resume retained the + same values and versions. + - Both stores Valid and ExternalSecrets SecretSynced; exact native read, sibling/listing + and wrong service account/namespace/store-use denials passed; reader revocation + and coding-agent deny precedence passed. + - Pinned image 7ff54c54e63e has one ready replica; protected previous config/image + retained and signing key unchanged. + - Both clients passed live JWKS and exact audience/subject/tenant/role/scope/900-second + lifetime checks, excess-scope and wrong-secret denials; human client consume + denied. + - Native verifier from pinned image ran in the attended owner process with memory-only + credentials. Pod network policy remains unchanged. Future iat bound is the existing + 30-second contract; expiry has zero leeway. + - Fresh post-rollout OpenBao OIDC login succeeded and self-revoked. No client-side + fetch front door or factory spending was admitted. + - 'Receipt: docs/evidence/2026-09-09-keycape-verifier-admission.json' lifecycle: deactivate: KeyCape disables the approval-engine-operator registration; platform detaches the policy from role external-secrets-keycape-approval-engine-operator diff --git a/docs/credential-lane-designs/keycape-approval-clients-review.md b/docs/credential-lane-designs/keycape-approval-clients-review.md index 8202cf3..9068fed 100644 --- a/docs/credential-lane-designs/keycape-approval-clients-review.md +++ b/docs/credential-lane-designs/keycape-approval-clients-review.md @@ -1,45 +1,28 @@ -# KeyCape approval-client custody: review packet +# KeyCape approval-client custody: accepted verifier delivery -Prepared 2026-09-09 by the-custodian. Both requests are **approved** by the user as platform operator and KeyCape -owner, explicitly recorded on 2026-09-09. The admission receipt is -[here](../evidence/2026-09-09-keycape-approval-admission.json). This is the review record for -RPF-WP-0035-T05, consumed by HFACT-WP-0001-T03. +Both CCR-2026-0017 and CCR-2026-0018 have the user's explicit approval as platform +operator and KeyCape owner. Both are now **verified**. Their existing decision +IDs remain resolved; no renewed review is needed for this completed scope. -| Request | Secret path and field | Client authority | Resolved decision | -| --- | --- | --- | --- | -| [CCR-2026-0017](../../credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml) | `platform/workloads/secrets-engine/approval-client`, `CLIENT_SECRET` | `approval:read`, `approval:consume` | `b533a271-b704-4c5c-98a2-9a5951aadfb6` | -| [CCR-2026-0018](../../credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml) | `platform/workloads/approval-engine/operator-client`, `CLIENT_SECRET` | create/read/approve/revoke/supersede/observe/emit; no consume | `efa90517-0cae-4eb6-a68d-5b0489c84d65` | +[Combined live receipt](../evidence/2026-09-09-keycape-verifier-admission.json) records version-1 custody, both ESO +stores and Secrets, exact native scope/auth/namespace denials, reader revocation, +compatible pinned KeyCape deployment and both service-client verification runs. +The existing human OpenBao login passed before and after the rollout. -Both named **platform-operator and key-cape-owner** reviews are approved. Each has its own -exact-path policy, Kubernetes auth role and ClusterSecretStore, bound to -`external-secrets/external-secrets`, limited to `sso`. ESO owns the resulting -Secret; authentication tokens have a 15-minute TTL. Client secrets require -explicit rotation or registration disablement; token expiry does not revoke them. +The two Warden fetch selectors remain unresolved: these CCRs authorize verifier +copies only. Client-side reads, audit custody, natural JWT expiry, real predecessor +rotation and factory operating/spend admission remain with the existing owner +records. See [the owner sequence](keycape-approval-clients.md). -The approved requests authorize verifier-side custody only. Client-side -retrieval, audit sender/receiver custody, operator `approval:consume`, adoption -of the Qonto Secret and factory spending remain outside these two requests. +The exercised procedure is `scripts/keycape_approval_custody.py`: `activate` for +first provision, `resume-activate` only with a completed rollback receipt, and +`verify` for existing custody. The selected kubeconfig must identify railiance01; +on this workstation it is `/home/worsch/.kube/config-railiance01`. Use the +Warden attended wrapper, a unique metadata receipt and protected recovery file. +The pinned native verifier is extracted from image digest `7ff54c54e63e...` and +hash-checked; its child environment carries credentials only during execution. +No Kubernetes egress policy changes are needed. -Technical review completed: both CCRs validate, their generated policies match -the two source HCL files, and the delivery manifests map exactly to the declared -paths and fields. The signed upstream issuer is verified; NetKingdom's exact live -pin is independently read back at Secret revision `58713343`. The compatible -KeyCape image and rollout patch are prepared. These checks are evidence for the -review, not substitutes for either named approval. - -Execution still follows the [owner rollout sequence](keycape-approval-clients.md) -through the founder-attended Warden/OpenBao envelope: policy/roles, CAS=0 initial -custody, Valid stores, SecretSynced delivery, compatible config/image replacement, -then positive and negative acceptance and metadata-only receipts. The contained owner command is now `scripts/keycape_approval_custody.py activate`. -It preflights both config and deployment changes, seeds with CAS=0, verifies native -and namespace boundaries, then calls NetKingdom's contained compatible rollout. -A failure restores the prior config/image pair before detaching verifier delivery; -initial KV versions remain in custody. The four new exact data/metadata deny -stanzas extend the coding-agent boundary without changing any grant. - -Validation: six checks against a disposable local OpenBao, eight config/rollback -tests, and real HTTP signature/claim/scope acceptance against the pinned KeyCape -image with synthetic keys. Production in-pod CLI and existing-human-login checks -remain part of the live window. The image exercise omits the in-pod CLI explicitly. -Use a unique private receipt and protected owner recovery path; a fresh attended -login after the rollout establishes the existing human path on the new build. +Validation: eight local OpenBao checks, ten config/rollback/clock checks, 53 +existing credential-change tests, plus a disposable pinned-image HTTPS exercise +that runs the exact native verifier with synthetic keys and client credentials. diff --git a/docs/credential-lane-designs/keycape-approval-clients.md b/docs/credential-lane-designs/keycape-approval-clients.md index e6c1080..c011453 100644 --- a/docs/credential-lane-designs/keycape-approval-clients.md +++ b/docs/credential-lane-designs/keycape-approval-clients.md @@ -1,14 +1,20 @@ # KeyCape approval-engine client custody admission +**2026-09-09 accepted:** verifier custody and the compatible image/config are live; +both service verifiers and a fresh existing-human OpenBao login passed. Both CCRs +are verified. [Live evidence](../evidence/2026-09-09-keycape-verifier-admission.json) supersedes the preparation +status below. Client-side reads and factory operating grants remain separate. + Answer to KEY-WP-0013-T02 (State Hub message `278a3ebe-b529-49f6-bd1a-e3ebcf318260`, KeyCape packet `key-cape: docs/approval-engine-provisioning-request.yaml`). Tracked here as RPF-WP-0035-T05. Requests: [CCR-2026-0017](../../credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml), [CCR-2026-0018](../../credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml). -Nothing below is an activation. Both CCRs are `proposed`; no value has been -generated, no KV version written, no manifest applied. Source preparation is not -live completion. +Both CCRs are **verified** following explicit user approval in both reviewer +roles. Version-1 custody, ESO delivery, the compatible KeyCape rollout and the +fresh existing-human login all passed. The contract below defines the completed +verifier-side scope; client-side reads remain a separate admission. ## a) Custody paths and field names @@ -53,7 +59,7 @@ keycape-rapp-qonto-client, key: client-secret}`. Read-only observation 2026-09-08, and it confirms KeyCape's own statement that the image carries only that one client reference. -Manifests are written and client-validated but unapplied: +Both delivery manifests are applied and live-verified: `argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml` and `keycape-approval-clients.externalsecrets.yaml`. @@ -146,9 +152,10 @@ ExternalSecrets, detach the policies from the roles. The KV versions are retaine until KeyCape confirms whether the registrations stay; if they are abandoned, KeyCape disables the registrations first and platform then destroys the versions. -**Date is not set here.** It depends on the founder's availability, which is not -mine to schedule. Propose a slot from 2026-09-10 and I will confirm the -platform side; the window needs roughly 60–90 minutes with both owners present. +The attended window completed on **2026-09-09** under the user's explicit +approval. Failed checks restored the compatible config/image and detached ESO +delivery. The final resume preserved both initial KV versions and passed all +service checks plus a fresh existing-human OpenBao login. ## What is not admitted @@ -184,4 +191,4 @@ approval is performed by this preflight. 2026-09-09: the live issuer pin is complete. The next review is captured in [keycape-approval-clients-review.md](keycape-approval-clients-review.md), with -one pending Hub decision per existing CCR and both required reviewer roles. +the resolved Hub decisions, both recorded reviewer roles and completed live verification. diff --git a/docs/evidence/2026-09-09-keycape-verifier-admission.json b/docs/evidence/2026-09-09-keycape-verifier-admission.json new file mode 100644 index 0000000..6ae6fc7 --- /dev/null +++ b/docs/evidence/2026-09-09-keycape-verifier-admission.json @@ -0,0 +1,220 @@ +{ + "schema": "helixforge.keycape-verifier-admission.v1", + "recorded_at": "2026-09-09T00:14:27.155338+00:00", + "approval": { + "schema": "railiance.keycape-custody-user-approval.v1", + "recorded_at": "2026-09-08T23:05:19.886216+00:00", + "user_response": "I approve, go on.", + "approved_question": "Do you approve CCR-2026-0017 and CCR-2026-0018, as platform operator and KeyCape owner, for the verifier-side credential delivery described in the review packet?", + "review_packet_revision": "52b24eab9a8aff3396e71b2296d0240a44f3b0db", + "roles": [ + "platform-operator", + "key-cape-owner" + ], + "scope": "Two verifier-side KeyCape client credential custody requests, including their governed attended provisioning and compatible rollout sequence.", + "client_side_read_authorized": false, + "factory_spending_authorized": false, + "requests": [ + { + "id": "CCR-2026-0017", + "decision_id": "b533a271-b704-4c5c-98a2-9a5951aadfb6", + "status": "approved", + "decision_status": "resolved", + "reviewed_roles": [ + "platform-operator", + "key-cape-owner" + ] + }, + { + "id": "CCR-2026-0018", + "decision_id": "efa90517-0cae-4eb6-a68d-5b0489c84d65", + "status": "approved", + "decision_status": "resolved", + "reviewed_roles": [ + "platform-operator", + "key-cape-owner" + ] + } + ] + }, + "activation": { + "schema": "platform.keycape-approval-custody.v1", + "status": "custody_and_service_acceptance_passed_pending_fresh_human_login", + "lanes": [ + { + "ccr": "CCR-2026-0017", + "source_sha256": "dc2bbd86e213edecc2f2f91a99d0f51136e9a0853a90e55f65dda2b942305416", + "custody_seeded": false, + "existing_version_reused": true, + "kv_version": 1, + "initial_request_id": "94c98373-33fa-591d-c201-d36dddf9879b", + "policy_applied": false, + "role_applied": true, + "native_reader_verified": true, + "cross_path_denied": true, + "parent_listing_denied": true, + "auth_ttl": 900, + "reader_revocation_verified": true, + "wrong_service_account_denied": true, + "coding_agent_deny_wins": true, + "store_ready": true, + "external_secret_ready": true, + "delivery_matches": true, + "secret_uid": "eaa28bdf-04af-4e4e-a1fc-fe395c7689a7", + "secret_resource_version": "58747058", + "wrong_namespace_denied": true, + "outside_namespace_store_denied": true + }, + { + "ccr": "CCR-2026-0018", + "source_sha256": "99148133ddf1eb15dce3c03862b51dc001818ca3121045035a0b0e5897c55dad", + "custody_seeded": false, + "existing_version_reused": true, + "kv_version": 1, + "initial_request_id": "a62ee5c5-b5ba-b7dc-0f51-e6fd8a6dde32", + "policy_applied": false, + "role_applied": true, + "native_reader_verified": true, + "cross_path_denied": true, + "parent_listing_denied": true, + "auth_ttl": 900, + "reader_revocation_verified": true, + "wrong_service_account_denied": true, + "coding_agent_deny_wins": true, + "store_ready": true, + "external_secret_ready": true, + "delivery_matches": true, + "secret_uid": "599a61a7-8244-4618-8c44-6473195bbe4e", + "secret_resource_version": "58747062", + "wrong_namespace_denied": true, + "outside_namespace_store_denied": true + } + ], + "started_at": "2026-09-09T00:09:28.852280+00:00", + "credential_values_emitted": false, + "client_side_read_admitted": false, + "keycape_rollout_completed": true, + "issuer_pin_revision": "58746187", + "phase": "awaiting_fresh_human_login", + "custody_versions_unchanged": true, + "namespace_probe_cleanup_requested": true, + "keycape": { + "existing_human_login_before": true, + "protected_recovery_retained": true, + "deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f", + "generation": 38, + "pod_uid": "8d62e972-d466-413e-a2e4-08b9a08016bc", + "single_ready_replica": true, + "acceptance_phase": "passed", + "clients": [ + { + "client_id": "secrets-engine-approval", + "live_jwks_signature_verified": true, + "exact_claims_verified": true, + "lifetime_seconds": 900, + "maximum_future_iat_seconds": 30, + "expiry_leeway_seconds": 0, + "excess_scope_denied": true, + "wrong_secret_denied": true, + "pinned_artifact_verifier_passed": true, + "verifier_location": "attended owner process", + "verifier_sha256": "4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92", + "real_predecessor_rotation_tested": false, + "observed_wall_clock_expiry": false + }, + { + "client_id": "approval-engine-operator", + "live_jwks_signature_verified": true, + "exact_claims_verified": true, + "lifetime_seconds": 900, + "maximum_future_iat_seconds": 30, + "expiry_leeway_seconds": 0, + "excess_scope_denied": true, + "wrong_secret_denied": true, + "pinned_artifact_verifier_passed": true, + "verifier_location": "attended owner process", + "verifier_sha256": "4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92", + "real_predecessor_rotation_tested": false, + "observed_wall_clock_expiry": false + } + ], + "acceptance_client": "approval-engine-operator", + "human_client_consume_denied": true, + "status": "service_acceptance_passed_pending_fresh_human_login", + "image": "forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611", + "config_resource_version": "58747126", + "signing_key_unchanged": true, + "unrelated_config_bytes_preserved": true, + "existing_human_login_after": false + }, + "finished_at": "2026-09-09T00:10:09.099481+00:00" + }, + "post_rollout_login": { + "schema": "netkingdom.keycape-approval-rollout.v1", + "status": "service_and_existing_human_login_acceptance_passed", + "values_emitted": false, + "deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f", + "generation": 38, + "pod_uid": "8d62e972-d466-413e-a2e4-08b9a08016bc", + "single_ready_replica": true, + "acceptance_phase": "passed", + "clients": [ + { + "client_id": "secrets-engine-approval", + "live_jwks_signature_verified": true, + "exact_claims_verified": true, + "lifetime_seconds": 900, + "maximum_future_iat_seconds": 30, + "expiry_leeway_seconds": 0, + "excess_scope_denied": true, + "wrong_secret_denied": true, + "pinned_artifact_verifier_passed": true, + "verifier_location": "attended owner process", + "verifier_sha256": "4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92", + "real_predecessor_rotation_tested": false, + "observed_wall_clock_expiry": false + }, + { + "client_id": "approval-engine-operator", + "live_jwks_signature_verified": true, + "exact_claims_verified": true, + "lifetime_seconds": 900, + "maximum_future_iat_seconds": 30, + "expiry_leeway_seconds": 0, + "excess_scope_denied": true, + "wrong_secret_denied": true, + "pinned_artifact_verifier_passed": true, + "verifier_location": "attended owner process", + "verifier_sha256": "4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92", + "real_predecessor_rotation_tested": false, + "observed_wall_clock_expiry": false + } + ], + "acceptance_client": "approval-engine-operator", + "human_client_consume_denied": true, + "existing_human_login_after": true, + "receipt_written_at": "2026-09-09T00:11:59.113549+00:00" + }, + "attended_envelope": { + "all_attempted_sessions_revoked": true, + "successful_activation_exit_code": 0, + "post_rollout_login_exit_code": 0 + }, + "validation": { + "local_openbao_tests": 8, + "configuration_and_recovery_tests": 10, + "credential_change_tests": 53, + "pinned_image_synthetic_https_and_native_verifier": "passed" + }, + "failed_attempt_receipts": [ + "net-kingdom:docs/evidence/2026-09-09-keycape-activation-attempts.json" + ], + "limits": { + "client_side_read_admitted": false, + "factory_spending_admitted": false, + "wall_clock_jwt_expiry_observed": false, + "actual_predecessor_rotation_observed": false + }, + "temporary_probe_namespaces_remaining": 0, + "owner_manifest_api_defaults_match_live": true +} diff --git a/scripts/keycape_approval_custody.py b/scripts/keycape_approval_custody.py index a367d05..b4aaff8 100644 --- a/scripts/keycape_approval_custody.py +++ b/scripts/keycape_approval_custody.py @@ -62,6 +62,36 @@ def contracts(): return result + +def resume_provision(lanes, receipt, prior_path): + prior = json.loads(Path(prior_path).read_text()) + require(prior.get('status') == 'failed' and prior.get('verifier_delivery_disabled_custody_versions_retained') + and prior.get('keycape', {}).get('compatible_pair_restored'), 'verified_partial_activation_receipt_required') + old_rows = prior['lanes'] + require(len(old_rows) == len(lanes), 'partial_receipt_lane_mismatch') + for lane, old in zip(lanes, old_rows): + require(old['ccr'] == lane['ccr'] and (old.get('custody_seeded') or old.get('existing_version_reused')) and old.get('kv_version') == 1, + 'initial_version_provenance_required') + metadata = read_optional(lane['metadata']) + require(metadata is not None and metadata['current_version'] == 1 + and not metadata['versions']['1'].get('destroyed') and not metadata['versions']['1'].get('deletion_time'), + 'initial_custody_version_changed') + role = read_optional('auth/kubernetes/role/' + lane['role']) + require(role is not None and role.get('token_policies') == [] + and role_matches(dict(role, token_policies=[lane['policy']]), lane), 'disabled_role_drift') + require(read_optional('sys/policies/acl/' + lane['policy'])['policy'] == lane['hcl'], 'resume_policy_drift') + require(all(re.search(r'path\s+"' + re.escape(path) + r'"\s*\{\s*capabilities\s*=\s*\["deny"\]\s*\}', read_optional(BOUNDARY)['policy']) + for path in (lane['kv'], lane['metadata'])), 'resume_boundary_drift') + # Only reattach the same reviewed reader roles. No KV write, import or rotation. + for lane, old in zip(lanes, old_rows): + bao(['write', 'auth/kubernetes/role/' + lane['role'], '-'], payload=lane['role_payload']) + require(role_matches(read_optional('auth/kubernetes/role/' + lane['role']), lane), 'resumed_role_readback_failed') + receipt['lanes'].append({'ccr': lane['ccr'], 'source_sha256': lane['source_sha256'], + 'custody_seeded': False, 'existing_version_reused': True, 'kv_version': 1, + 'initial_request_id': old.get('initial_request_id', old.get('request_id')), 'policy_applied': False, 'role_applied': True}) + receipt['custody_versions_unchanged'] = True + + def read_optional(path): result = bao(['read', '-format=json', path], allow_failure=True) if result.returncode == 0: @@ -264,8 +294,9 @@ def run(args, receipt): check = data(command(['python3', '-B', '/home/worsch/net-kingdom/sso-mfa/k8s/keycape/openbao-client-config.py', 'issuer-check-live'], env=env)) require(check['issuer_matches'] and check['verified_issuer'] == 'https://auth.coulomb.social', 'verified_issuer_pin_required') receipt['issuer_pin_revision'] = check['before']['resource_version'] - rollout = load_rollout() if args.action == 'activate' else None + rollout = load_rollout() if args.action in {'activate', 'resume-activate'} else None if rollout: + rollout.verify_artifact() # Exercise config construction and API admission before any custody write. secret = rollout.get(kube, 'secret', 'keycape-config') dep = rollout.get(kube, 'deployment', 'keycape') @@ -282,6 +313,9 @@ def run(args, receipt): receipt['phase'] = 'provision' if args.action in {'provision', 'activate'}: provision(lanes, receipt) + elif args.action == 'resume-activate': + require(args.prior_receipt, 'prior_receipt_required') + resume_provision(lanes, receipt, args.prior_receipt) else: receipt['lanes'] = [{'ccr': lane['ccr']} for lane in lanes] receipt['phase'] = 'native_verification' @@ -307,10 +341,11 @@ def run(args, receipt): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('action', choices=['provision', 'verify', 'activate']) + parser.add_argument('action', choices=['provision', 'verify', 'activate', 'resume-activate']) parser.add_argument('--kubeconfig', required=True) parser.add_argument('--receipt', required=True) parser.add_argument('--recovery') + parser.add_argument('--prior-receipt') args = parser.parse_args() receipt = {'schema': 'platform.keycape-approval-custody.v1', 'status': 'failed', 'lanes': [], 'started_at': datetime.now(timezone.utc).isoformat(), 'credential_values_emitted': False, diff --git a/tests/test_keycape_approval_custody.py b/tests/test_keycape_approval_custody.py index cdf2a2a..a161540 100644 --- a/tests/test_keycape_approval_custody.py +++ b/tests/test_keycape_approval_custody.py @@ -1,6 +1,8 @@ """Opt-in local OpenBao exercise; no production API or credential helper used.""" import importlib.util import json +import copy +import tempfile import os from pathlib import Path import secrets @@ -78,6 +80,7 @@ class CustodyExercise(unittest.TestCase): self.assertTrue(values[0] != values[1]) self.assertTrue(all(x['kv_version'] == 1 for x in receipt['lanes'])) self.assertFalse(any(value in json.dumps(receipt) for value in values)) + type(self).initial_receipt = copy.deepcopy(receipt) def test_02_retry_refuses_existing_custody(self): with self.assertRaisesRegex(lane.LaneError, 'existing_custody_requires'): @@ -110,5 +113,26 @@ class CustodyExercise(unittest.TestCase): with self.assertRaisesRegex(lane.LaneError, 'attended_warden_envelope_required'): lane.run(None, {}) + def test_07_resume_reattaches_readers_without_rewriting_values(self): + before = [lane.read_optional(x['metadata']) for x in self.lanes] + for item in self.lanes: + lane.bao(['write', 'auth/kubernetes/role/' + item['role'], '-'], payload=dict(item['role_payload'], policies=[])) + prior = dict(self.initial_receipt, status='failed', verifier_delivery_disabled_custody_versions_retained=True, + keycape={'compatible_pair_restored': True}) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'prior.json'; path.write_text(json.dumps(prior)) + receipt = {'lanes': []} + lane.resume_provision(self.lanes, receipt, path) + self.assertTrue(receipt['custody_versions_unchanged']) + self.assertTrue(all(x['existing_version_reused'] and not x['custody_seeded'] for x in receipt['lanes'])) + self.assertEqual(before, [lane.read_optional(x['metadata']) for x in self.lanes]) + self.assertTrue(all(lane.role_matches(lane.read_optional('auth/kubernetes/role/' + x['role']), x) for x in self.lanes)) + + def test_08_resume_requires_completed_rollback(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'prior.json'; path.write_text(json.dumps({'status':'failed'})) + with self.assertRaisesRegex(lane.LaneError, 'verified_partial_activation_receipt_required'): + lane.resume_provision(self.lanes, {'lanes': []}, path) + if __name__ == '__main__': unittest.main() diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index 51fc6d5..a29fc8f 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -7,7 +7,7 @@ repo: railiance-platform status: blocked owner: codex created: "2026-09-05" -updated: "2026-09-08" +updated: "2026-09-09" related: - RPF-WP-0032 - RPF-WP-0033 @@ -228,6 +228,18 @@ procedure is exercised and the admitted attended rollout is carried through. Verifier-side scope, separate client-side/audit lanes and live acceptance remain as defined in the reviewed requests. +2026-09-09 verifier-side return: both CCRs are verified after recorded user approval, +CAS=0 version-1 custody, native read/auth denials and revocation, Valid stores, +SecretSynced delivery, compatible KeyCape rollout and live positive/negative +service checks. Existing human OpenBao login passed again on the new image. +Receipt: `docs/evidence/2026-09-09-keycape-verifier-admission.json`. Failed attempts restored the compatible +configuration/image and detached delivery; final resume reused version 1. + +T05 remains progress for the separately admitted client-side read lanes and +associated owner handoffs. Neither Warden fetch selector is resolvable through +these verifier-only CCRs. Do not re-request the completed two named reviews or +reseed these paths. Rotation is a distinct, version-guarded operation. + ## Dependency review — 2026-09-06 SECRETS-WP-0008-T02 now records the local PIP claim/validation join implemented