feat: accept KeyCape approval clients with tested recovery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 02:18:12 +02:00
parent ce826cfa8d
commit 303a584bd0
8 changed files with 810 additions and 20 deletions

View file

@ -0,0 +1,316 @@
{
"attempts": [
{
"schema": "platform.keycape-approval-custody.v1",
"status": "failed",
"lanes": [],
"started_at": "2026-09-08T23:34:28.315053+00:00",
"credential_values_emitted": false,
"client_side_read_admitted": false,
"keycape_rollout_completed": false,
"error": "command_failed",
"finished_at": "2026-09-08T23:34:28.559853+00:00"
},
{
"schema": "platform.keycape-approval-custody.v1",
"status": "failed",
"lanes": [
{
"ccr": "CCR-2026-0017",
"source_sha256": "dc2bbd86e213edecc2f2f91a99d0f51136e9a0853a90e55f65dda2b942305416",
"policy_applied": true,
"role_applied": true,
"custody_seeded": true,
"kv_version": 1,
"request_id": "94c98373-33fa-591d-c201-d36dddf9879b",
"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": "34502b48-ed5d-4a1d-bc43-aa282392775e",
"secret_resource_version": "58736028",
"wrong_namespace_denied": true,
"outside_namespace_store_denied": true
},
{
"ccr": "CCR-2026-0018",
"source_sha256": "99148133ddf1eb15dce3c03862b51dc001818ca3121045035a0b0e5897c55dad",
"policy_applied": true,
"role_applied": true,
"custody_seeded": true,
"kv_version": 1,
"request_id": "a62ee5c5-b5ba-b7dc-0f51-e6fd8a6dde32",
"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": "e6f42c4b-dbca-480d-96a4-c475f1298e7f",
"secret_resource_version": "58736036",
"wrong_namespace_denied": true,
"outside_namespace_store_denied": true
}
],
"started_at": "2026-09-08T23:36:01.727953+00:00",
"credential_values_emitted": false,
"client_side_read_admitted": false,
"keycape_rollout_completed": false,
"issuer_pin_revision": "58713343",
"phase": "keycape_rollout",
"coding_agent_boundary_extended_only_to_new_paths": true,
"initial_values_generation": "independent CSPRNG 48-byte values; memory-to-OpenBao stdin only; CAS=0",
"namespace_probe_cleanup_requested": true,
"keycape": {
"existing_human_login_before": true,
"protected_recovery_retained": true,
"compatible_pair_restored": true
},
"verifier_delivery_disabled_custody_versions_retained": true,
"error": "keycape_readiness_timeout",
"finished_at": "2026-09-08T23:39:24.210112+00:00"
},
{
"schema": "platform.keycape-approval-custody.v1",
"status": "failed",
"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": "b85ce6bb-45c4-49ab-81c9-e92f001500b2",
"secret_resource_version": "58741915",
"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": "faad6d54-a6b1-4a3d-9102-4e35aeea67a5",
"secret_resource_version": "58741919",
"wrong_namespace_denied": true,
"outside_namespace_store_denied": true
}
],
"started_at": "2026-09-08T23:54:16.437123+00:00",
"credential_values_emitted": false,
"client_side_read_admitted": false,
"keycape_rollout_completed": false,
"issuer_pin_revision": "58737048",
"phase": "keycape_rollout",
"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": 32,
"pod_uid": "28a62a5a-1ac6-4d6c-a1e1-4a414a621559",
"single_ready_replica": true,
"clients": [],
"compatible_pair_restored": true
},
"verifier_delivery_disabled_custody_versions_retained": true,
"error": "contained_operation_failed",
"finished_at": "2026-09-08T23:55:02.310001+00:00"
},
{
"schema": "platform.keycape-approval-custody.v1",
"status": "failed",
"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": "827b4e04-6727-4dd4-9cdb-2554ecc84441",
"secret_resource_version": "58743272",
"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": "ca949cda-6a4f-4c24-9a06-6a9c7e00e4aa",
"secret_resource_version": "58743276",
"wrong_namespace_denied": true,
"outside_namespace_store_denied": true
}
],
"started_at": "2026-09-08T23:58:13.955525+00:00",
"credential_values_emitted": false,
"client_side_read_admitted": false,
"keycape_rollout_completed": false,
"issuer_pin_revision": "58742089",
"phase": "keycape_rollout",
"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": 34,
"pod_uid": "dabd15a0-f9c3-47f2-82ee-e53590754558",
"single_ready_replica": true,
"acceptance_phase": "in_pod_verifier",
"clients": [],
"acceptance_client": "secrets-engine-approval",
"failure_class": "LaneError",
"compatible_pair_restored": true
},
"verifier_delivery_disabled_custody_versions_retained": true,
"error": "command_failed",
"finished_at": "2026-09-08T23:59:06.395926+00:00"
},
{
"schema": "platform.keycape-approval-custody.v1",
"status": "failed",
"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": "1e0c888a-c35b-4fe2-8fc5-c510256c19df",
"secret_resource_version": "58745995",
"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": "50b18e82-6187-47d1-9b07-199f9b999f47",
"secret_resource_version": "58745999",
"wrong_namespace_denied": true,
"outside_namespace_store_denied": true
}
],
"started_at": "2026-09-09T00:06:30.650774+00:00",
"credential_values_emitted": false,
"client_side_read_admitted": false,
"keycape_rollout_completed": false,
"issuer_pin_revision": "58743475",
"phase": "keycape_rollout",
"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": 36,
"pod_uid": "011de4b4-c2cd-429d-ba04-e7c72b2f1f9e",
"single_ready_replica": true,
"acceptance_phase": "signature_and_claims",
"clients": [],
"acceptance_client": "secrets-engine-approval",
"failure_class": "ImmatureSignatureError",
"compatible_pair_restored": true
},
"verifier_delivery_disabled_custody_versions_retained": true,
"error": "contained_operation_failed",
"finished_at": "2026-09-09T00:07:25.670140+00:00"
}
],
"all_sessions_revoked": true
}

View file

@ -1,13 +1,18 @@
{
"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,
"pod_verify_client_passed": false,
"pinned_artifact_verifier_passed": true,
"verifier_location": "attended owner process",
"verifier_sha256": "4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92",
"real_predecessor_rotation_tested": false,
"observed_wall_clock_expiry": false
},
@ -16,15 +21,20 @@
"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,
"pod_verify_client_passed": false,
"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,
"target": "disposable pinned KeyCape image; synthetic keys and clients only",
"pod_cli_verification": "deferred to live in-pod check",
"pinned_native_cli_verification": "passed against loopback HTTPS with synthetic credentials",
"cleanup_complete": true
}

View file

@ -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
}

View file

@ -0,0 +1,78 @@
# KeyCape approval-client rollout and recovery
Accepted 2026-09-09 under CCR-2026-0017 and CCR-2026-0018. Both service
registrations are live on one ready KeyCape replica. The existing human OpenBao
login passed after replacement. [Live receipt](evidence/2026-09-09-keycape-verifier-admission.json).
The [attempt receipts](evidence/2026-09-09-keycape-activation-attempts.json) retain
failed checks and confirmed compatible rollback; no failed attempt is counted
as activation. Both OpenBao values remain at their initial version 1.
The source image is the immutable manifest
`forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`.
Deployment authority is `sso-mfa/k8s/keycape/deployment.yaml`; the live config
remains in `sso/keycape-config`. The contained helper appends only the two
reviewed client registrations and preserves the signing key and every unrelated
configuration byte. It uses UID/resourceVersion tests for config and deployment
mutations, and retains the previous compatible pair in protected owner storage.
The platform command `scripts/keycape_approval_custody.py` opens through
`openbao-attended-exec.py` and the governed Warden `openbao-platform-admin-login`
envelope. It owns approved policies/roles, CAS=0 first provision, ESO delivery,
native scope/auth/namespace denials and reader revocation. It invokes this
repository's `sso-mfa/k8s/keycape/approval-clients-rollout.py` for image/config
replacement and service acceptance. Do not run the platform child directly.
On this workstation the verified Railiance kubeconfig is
`/home/worsch/.kube/config-railiance01`; the default config targets another endpoint.
Use `activate` only for a first provision with absent custody and delivery.
`resume-activate --prior-receipt <metadata receipt>` requires a completed compatible
rollback, unchanged version-1 custody, unchanged policies and detached reader roles.
It reattaches the same readers without writing any value. Both actions require a
unique `--receipt` and `--recovery` path. Successful service acceptance is followed
by a fresh attended login running the owner helper's `verify-after-login` action.
Existing active custody can be rechecked with the platform helper's `verify`
action. Repeating first provision against active custody is deliberately refused.
The verifier binary comes from the pinned image, without starting its extraction
container. Install it in the owner-protected path
`~/.local/share/key-cape/verified-bin/dcebd46/keycape` with mode 0700. Its SHA-256
must be `4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92`;
the helper checks this before custody mutation and before each verifier run.
Extraction needs only `docker create`, `docker cp <container>:/keycape <private path>`,
and removal of that never-started container. No credential enters that container.
Run the native verifier in the existing attended owner process. It receives the
same verifier-side credential already read privately for independent JWT checks,
through a child environment that is never printed or written to a file. The
KeyCape pod's internal-only egress policy does not admit its public HTTPS issuer;
this rollout makes no network-policy change. Container readiness checks the
manifest digest in `imageID`, since containerd's `image` field can hold a different
runtime configuration digest.
Independent JWT checks match the existing native contract: at most 30 seconds
of future issued-at time, strict not-before and expiry, exact audience/subject/
tenant/role/scope and a 900-second lifetime. Global JWT leeway is not used.
Wrong secrets and excess scopes require the exact issuer HTTP status/error/feature.
The public human OpenBao client is also denied `approval:consume`.
Reproducible preparation checks:
```sh
python3 -B sso-mfa/k8s/keycape/test_approval_clients_rollout.py
python3 -B sso-mfa/k8s/keycape/exercise-approval-clients.py --receipt /tmp/<unique-scratch-receipt>.json
```
The second command requires Docker, the installed pinned verifier, PyYAML,
PyJWT and cryptography. It runs the pinned image behind loopback HTTPS with
synthetic signing keys and client credentials, including the native verifier.
It removes its container, TLS server and temporary credential files. It neither
reads production config nor opens an attended login. The platform's
`tests/test_keycape_approval_custody.py` separately exercises real ACLs, CAS=0,
revocation and version-preserving resume against disposable local OpenBao.
KEY-WP-0013-T02 is complete. KEY-WP-0013-T05 retains the approval UI's callback
and MFA/PKCE acceptance. Separate client-side delivery remains RPF-WP-0035-T05;
audit custody remains AUDIT-WP-0009-T09; claim/consume and native execution proof
remain APPROVAL-WP-0002 and SECRETS-WP-0009-T03. Initial provisioning did not
exercise natural JWT expiry or actual predecessor rotation. HFACT-WP-0001 retains
factory execution/grant/spend admission and Railiance worker acceptance.

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import argparse
import base64
import copy
import hashlib
import importlib.util
import json
import os
@ -26,11 +27,18 @@ spec = importlib.util.spec_from_file_location('pin', Path(__file__).with_name('o
pin = importlib.util.module_from_spec(spec); spec.loader.exec_module(pin)
ISSUER = 'https://kc.coulomb.social'
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611'
VERIFIER = Path('/home/worsch/.local/share/key-cape/verified-bin/dcebd46/keycape')
VERIFIER_SHA256 = '4bf93bbe9afe0bf2e21d51c03373a7e2b4be6864586bcc2d472eeb9abc913d92'
PRIOR_IMAGE = 'forgejo.coulomb.social/coulomb/key-cape:main-153258b'
IDS = ('secrets-engine-approval', 'approval-engine-operator')
SECRET_NAMES = ('keycape-secrets-engine-approval-client', 'keycape-approval-engine-operator-client')
def verify_artifact():
require(VERIFIER.is_file() and not VERIFIER.is_symlink()
and hashlib.sha256(VERIFIER.read_bytes()).hexdigest() == VERIFIER_SHA256, 'pinned_verifier_artifact_required')
def registrations():
source = yaml.safe_load((KEYCAPE / 'config/service-clients.example.yaml').read_text())['clients']
result = [next(c for c in source if c['clientId'] == name) for name in IDS]
@ -115,7 +123,9 @@ def ready(kube, image, timeout=150):
podlist = data(command(kube + ['-n', 'sso', 'get', 'pods', '-l', 'app.kubernetes.io/name=keycape', '-o', 'json']))['items']
if len(podlist) == 1 and not podlist[0]['metadata'].get('deletionTimestamp'):
containers = podlist[0].get('status', {}).get('containerStatuses', [])
if any(c['name'] == 'keycape' and c['ready'] and c['image'] == image for c in containers):
if any(c['name'] == 'keycape' and c['ready'] and
(c.get('imageID', '').removeprefix('docker-pullable://') == image
if '@sha256:' in image else c.get('image') == image) for c in containers):
return {'deployment_uid': dep['metadata']['uid'], 'generation': dep['metadata']['generation'],
'pod_uid': podlist[0]['metadata']['uid'], 'single_ready_replica': True}
time.sleep(3)
@ -141,7 +151,10 @@ def http(path, fields=None, credentials=None):
with opener.open(request, timeout=20) as response:
return response.status, json.load(response)
except urllib.error.HTTPError as error:
return error.code, json.load(error)
try:
return error.code, json.load(error)
except (ValueError, TypeError):
raise LaneError('http_' + str(error.code) + '_non_json_error') from None
def denied(result, status, feature):
@ -150,53 +163,79 @@ def denied(result, status, feature):
and body.get('feature') == feature, 'keycape_denial_inconclusive')
def verified_claims(token, public_key):
# Match authclient/client.go: tolerate at most 30s future iat, but no
# extension to expiry or not-before. PyJWT's global leeway would extend both.
claims = jwt.decode(token, public_key, algorithms=['RS256'], issuer=ISSUER, audience='approval-engine',
options={'verify_iat': False, 'require': ['exp', 'iat', 'sub', 'iss', 'aud']})
require(type(claims['iat']) is int and type(claims['exp']) is int
and claims['iat'] <= int(time.time()) + 30 and claims['iat'] < claims['exp'], 'issued_at_binding_failed')
return claims
def acceptance(kube, receipt):
receipt['acceptance_phase'] = 'discovery'
code, discovery = http('/.well-known/openid-configuration')
require(code == 200 and discovery['issuer'] == ISSUER
and discovery['token_endpoint'] == ISSUER + '/token'
and discovery['jwks_uri'].startswith(ISSUER + '/'), 'discovery_binding_mismatch')
receipt['acceptance_phase'] = 'jwks'
code, jwks = http(discovery['jwks_uri'].removeprefix(ISSUER))
require(code == 200, 'jwks_failed')
receipt['clients'] = []
for client, secret_name in zip(registrations(), SECRET_NAMES):
receipt['acceptance_client'] = client['clientId']
receipt['acceptance_phase'] = 'verifier_secret_read'
secret = get(kube, 'secret', secret_name)
credential = base64.b64decode(secret['data']['client-secret'], validate=True).decode()
scopes = ' '.join(client['allowedScopes'])
receipt['acceptance_phase'] = 'token_exchange'
result, response = http('/token', {'grant_type': 'client_credentials', 'scope': scopes}, (client['clientId'], credential))
require(result == 200 and response.get('token_type') == 'Bearer' and response.get('expires_in') == 900, 'service_issuance_failed')
receipt['acceptance_phase'] = 'signature_and_claims'
token = response['access_token']; header = jwt.get_unverified_header(token)
keys = [key for key in jwks['keys'] if key['kid'] == header.get('kid')]
require(header.get('alg') == 'RS256' and len(keys) == 1, 'signing_key_selection_failed')
claims = jwt.decode(token, jwt.PyJWK.from_dict(keys[0]).key, algorithms=['RS256'],
issuer=ISSUER, audience='approval-engine',
options={'require': ['exp', 'iat', 'sub', 'iss', 'aud']})
claims = verified_claims(token, jwt.PyJWK.from_dict(keys[0]).key)
require(claims['sub'] == client['serviceSubject'] and claims['tenant'] == 'tenant:platform'
and claims['aud'] == 'approval-engine'
and claims['roles'] == client['roles'] and claims['exp'] - claims['iat'] == 900
and claims['principal_type'] == 'service'
and set(claims['scope'].split()) == set(client['allowedScopes']), 'exact_claims_mismatch')
excessive = 'approval:approve' if client['clientId'] == IDS[0] else 'approval:consume'
receipt['acceptance_phase'] = 'excess_scope_denial'
denied(http('/token', {'grant_type': 'client_credentials', 'scope': excessive},
(client['clientId'], credential)), 400, 'scope')
receipt['acceptance_phase'] = 'wrong_secret_denial'
denied(http('/token', {'grant_type': 'client_credentials', 'scope': scopes},
(client['clientId'], secrets.token_urlsafe(48))), 401, 'Authorization')
args = kube + ['-n', 'sso', 'exec', 'deployment/keycape', '--', '/keycape', 'verify-client',
args = [str(VERIFIER), 'verify-client',
'-issuer', ISSUER, '-client-id', client['clientId'], '-audience', 'approval-engine', '-scope', scopes,
'-secret-env', client['secretRef'].removeprefix('env:'), '-expect-subject', client['serviceSubject'],
'-expect-tenant', 'tenant:platform', '-expect-roles', ','.join(client['roles']), '-deny-scope', excessive]
command(args)
receipt['acceptance_phase'] = 'pinned_artifact_verifier'
verify_artifact()
verifier_env = os.environ.copy()
verifier_env[client['secretRef'].removeprefix('env:')] = credential
command(args, env=verifier_env)
receipt['clients'].append({'client_id': client['clientId'], 'live_jwks_signature_verified': True,
'exact_claims_verified': True, 'lifetime_seconds': 900, 'excess_scope_denied': True,
'wrong_secret_denied': True, 'pod_verify_client_passed': 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': VERIFIER_SHA256,
'real_predecessor_rotation_tested': False, 'observed_wall_clock_expiry': False})
receipt['acceptance_phase'] = 'human_consume_denial'
query = urllib.parse.urlencode({'client_id': 'openbao-admin', 'response_type': 'code',
'redirect_uri': 'http://localhost:8250/oidc/callback', 'scope': 'openid approval:consume',
'code_challenge_method': 'S256', 'code_challenge': 'A' * 43, 'state': secrets.token_urlsafe(32)})
denied(http('/authorize?' + query), 400, 'scope')
receipt['human_client_consume_denied'] = True
receipt['acceptance_phase'] = 'passed'
def rollout(kube, receipt, recovery_path):
assert_cluster(kube)
verify_artifact()
before = get(kube, 'secret', 'keycape-config')
deployment = get(kube, 'deployment', 'keycape')
require(before['metadata']['uid'] == '2e94519d-1550-41c7-9701-2efe47fe1fd3'
@ -223,7 +262,8 @@ def rollout(kube, receipt, recovery_path):
receipt.update(status='service_acceptance_passed_pending_fresh_human_login', image=IMAGE,
config_resource_version=after['metadata']['resourceVersion'], signing_key_unchanged=True,
unrelated_config_bytes_preserved=True, existing_human_login_after=False)
except Exception:
except Exception as failure:
receipt['failure_class'] = type(failure).__name__
# Read after uncertain API outcomes too; never assume a timeout means no write.
now = get(kube, 'secret', 'keycape-config')
require(now['metadata']['uid'] == before['metadata']['uid'], 'rollback_config_identity_drift')

View file

@ -1,8 +1,8 @@
# Deployment + Service — KeyCape (namespace: sso)
#
# KeyCape is the OIDC orchestration layer. It is stateless: all persistent
# state lives in Authelia (session), LLDAP (users), and privacyIDEA (MFA tokens).
# No PVC is required.
# KeyCape orchestrates OIDC. Pending logins and authorization codes are process-local;
# use one replica with Recreate during replacement. Persistent identity state remains
# in Authelia, LLDAP and privacyIDEA. No PVC is required.
#
# Configuration is stored entirely in the keycape-config Secret, which holds
# a complete config.yaml and the RSA private key used to sign OIDC tokens
@ -33,7 +33,7 @@ spec:
matchLabels:
app.kubernetes.io/name: keycape
strategy:
type: RollingUpdate # stateless — safe to roll
type: Recreate # one issuer instance; process-local login/code state
template:
metadata:
labels:
@ -50,7 +50,7 @@ spec:
- name: keycape
# Image published to the self-hosted Forgejo OCI registry (KEY-WP-0002).
# KEY-WP-0012: canonical OIDC subject resolution for /userinfo.
image: forgejo.coulomb.social/coulomb/key-cape:main-153258b
image: forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611
imagePullPolicy: IfNotPresent
ports:
@ -67,6 +67,17 @@ spec:
name: keycape-rapp-qonto-client
key: client-secret
- name: KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: keycape-secrets-engine-approval-client
key: client-secret
- name: KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: keycape-approval-engine-operator-client
key: client-secret
volumeMounts:
# keycape-config Secret provides config.yaml and key.pem
- name: config-secret
@ -89,7 +100,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
path: /readyz
port: 8080
initialDelaySeconds: 0
periodSeconds: 10

View file

@ -0,0 +1,92 @@
import base64, importlib.util, json, os, secrets, socket, subprocess, tempfile, time
from pathlib import Path
from unittest.mock import patch
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import yaml
import http.server, ssl, threading, urllib.request, urllib.error, datetime, ipaddress
from cryptography import x509
from cryptography.x509.oid import NameOID
import argparse
parser=argparse.ArgumentParser(description='Exercise the pinned image and verifier over loopback HTTPS using synthetic credentials only.')
parser.add_argument('--receipt', required=True, type=Path)
args=parser.parse_args()
spec=importlib.util.spec_from_file_location('rollout',Path(__file__).with_name('approval-clients-rollout.py'))
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
m.verify_artifact()
def call(args):
p=subprocess.run(args,capture_output=True,timeout=45)
if p.returncode: raise RuntimeError('contained_docker_command_failed')
return p
sock=socket.socket(); sock.bind(('127.0.0.1',0)); port=sock.getsockname()[1]; sock.close()
class Proxy(http.server.BaseHTTPRequestHandler):
def log_message(self, *args): pass
def handle_proxy(self):
body = self.rfile.read(int(self.headers.get('Content-Length', 0))) if self.command == 'POST' else None
req = urllib.request.Request(f'http://127.0.0.1:{port}'+self.path, data=body,
headers={k:self.headers[k] for k in ['Content-Type','Authorization'] if k in self.headers})
try:
response=urllib.request.urlopen(req,timeout=20)
except urllib.error.HTTPError as error: response=error
with response:
self.send_response(response.code)
self.send_header('Content-Type', response.headers.get('Content-Type','application/json'))
self.end_headers(); self.wfile.write(response.read())
do_GET=handle_proxy
do_POST=handle_proxy
server=http.server.ThreadingHTTPServer(('127.0.0.1',0),Proxy)
https_port=server.server_address[1]
name='keycape-approval-exercise-'+secrets.token_hex(5)
with tempfile.TemporaryDirectory(prefix='keycape-private-exercise-') as temp:
root=Path(temp)
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
(root/'key.pem').write_bytes(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()))
os.chmod(root/'key.pem',0o600)
name_attr=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'KeyCape local exercise')])
cert=(x509.CertificateBuilder().subject_name(name_attr).issuer_name(name_attr).public_key(key.public_key())
.serial_number(x509.random_serial_number()).not_valid_before(datetime.datetime.now(datetime.timezone.utc)-datetime.timedelta(minutes=1))
.not_valid_after(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(hours=1))
.add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address('127.0.0.1'))]),False)
.add_extension(x509.BasicConstraints(ca=True,path_length=None),True))
from cryptography.hazmat.primitives import hashes
(root/'cert.pem').write_bytes(cert.sign(key,hashes.SHA256()).public_bytes(serialization.Encoding.PEM))
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); context.load_cert_chain(root/'cert.pem',root/'key.pem')
server.socket=context.wrap_socket(server.socket,server_side=True)
threading.Thread(target=server.serve_forever,daemon=True).start()
config=yaml.safe_load((m.KEYCAPE/'config/dev-config.yaml').read_text())
config.update(issuer=f'https://127.0.0.1:{https_port}')
config['authelia']['issuer']='https://auth.coulomb.social'
config['clients'].extend(m.registrations())
(root/'config.yaml').write_text(yaml.safe_dump(config,sort_keys=False))
os.chmod(root/'config.yaml',0o600)
values={c['clientId']:secrets.token_urlsafe(48) for c in m.registrations()}
(root/'env').write_text('KEYCAPE_CONFIG=/etc/keycape/config.yaml\n'+''.join(c['secretRef'].removeprefix('env:')+'='+values[c['clientId']]+'\n' for c in m.registrations()))
os.chmod(root/'env',0o600)
created=False
try:
call(['docker','run','-d','--rm','--name',name,'--user',str(os.getuid()),'--publish',f'127.0.0.1:{port}:8080',
'--env-file',str(root/'env'),'--mount','type=bind,source='+temp+',target=/etc/keycape,readonly',m.IMAGE])
created=True
with patch.object(m,'ISSUER',config['issuer']), patch.dict(os.environ,{'SSL_CERT_FILE':str(root/'cert.pem')}):
for _ in range(30):
try:
status,_=m.http('/.well-known/openid-configuration')
if status==200: break
except Exception: pass
time.sleep(.5)
else: raise RuntimeError('scratch_keycape_not_ready')
def fake_get(kube,kind,name):
index=m.SECRET_NAMES.index(name)
return {'data': {'client-secret':base64.b64encode(values[m.IDS[index]].encode()).decode()}}
receipt={}
with patch.object(m,'get',side_effect=fake_get):
m.acceptance([],receipt)
receipt.update(target='disposable pinned KeyCape image; synthetic keys and clients only',
pinned_native_cli_verification='passed against loopback HTTPS with synthetic credentials',cleanup_complete=False)
finally:
if created: call(['docker','stop','--time=5',name])
server.shutdown(); server.server_close()
receipt['cleanup_complete']=True
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
with os.fdopen(fd,'w') as out: out.write(json.dumps(receipt,indent=2)+'\n')
print('Pinned-image scratch acceptance passed; synthetic credentials removed.')

View file

@ -83,7 +83,7 @@ class RolloutTests(unittest.TestCase):
changed['metadata']['resourceVersion'] = str(int(changed['metadata']['resourceVersion']) + 1)
state[kind] = changed
return copy.deepcopy(changed)
with tempfile.TemporaryDirectory() as directory, patch.object(m, 'assert_cluster'), \
with tempfile.TemporaryDirectory() as directory, patch.object(m, 'assert_cluster'), patch.object(m, 'verify_artifact'), \
patch.object(m, 'get', side_effect=lambda kube, kind, name: copy.deepcopy(state[kind])), \
patch.object(m, 'patch_object', side_effect=fake_patch), patch.object(m, 'ready', return_value={}), \
patch.object(m, 'acceptance', side_effect=m.LaneError('synthetic_acceptance_failure')):
@ -95,6 +95,29 @@ class RolloutTests(unittest.TestCase):
self.assertEqual(state['deployment']['spec'], original['deployment']['spec'])
self.assertEqual((Path(directory) / 'recovery.json').stat().st_mode & 0o777, 0o600)
def test_ready_matches_manifest_imageid_not_runtime_config_id(self):
dep = deployment(); dep['metadata']['generation'] = 30
dep['status'] = {'observedGeneration': 30, 'updatedReplicas': 1, 'readyReplicas': 1, 'availableReplicas': 1, 'replicas': 1}
pod = {'metadata': {'uid': 'fixture-pod'}, 'status': {'containerStatuses': [
{'name': 'keycape', 'ready': True, 'image': 'sha256:runtime-config-id', 'imageID': m.IMAGE}]}}
from types import SimpleNamespace
with patch.object(m, 'get', return_value=dep), patch.object(m, 'command', return_value=SimpleNamespace(stdout=json.dumps({'items': [pod]}).encode())):
self.assertTrue(m.ready([], m.IMAGE, timeout=1)['single_ready_replica'])
def test_iat_skew_matches_native_contract_without_extending_expiry(self):
from cryptography.hazmat.primitives.asymmetric import rsa
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
now = int(m.time.time())
claims = {'iss': m.ISSUER, 'aud': 'approval-engine', 'sub': 'synthetic-service', 'iat': now + 2, 'exp': now + 902}
encoded = m.jwt.encode(claims, key, algorithm='RS256')
self.assertEqual(m.verified_claims(encoded, key.public_key())['iat'], now + 2)
future = m.jwt.encode(dict(claims, iat=now+60), key, algorithm='RS256')
with self.assertRaisesRegex(m.LaneError, 'issued_at_binding_failed'):
m.verified_claims(future, key.public_key())
expired = m.jwt.encode(dict(claims, iat=now-900, exp=now-1), key, algorithm='RS256')
with self.assertRaises(m.jwt.ExpiredSignatureError):
m.verified_claims(expired, key.public_key())
def test_unrelated_refusal_never_passes(self):
for status, body in [(500, {}), (400, {'error': 'invalid_profile_usage', 'feature': 'client_id'}),
(401, {'error': 'invalid_profile_usage', 'feature': 'scope'})]: