feat: admit and exercise KeyCape verifier custody activation
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
codex 2026-09-09 01:32:45 +02:00
parent 52b24eab9a
commit b7861de20e
8 changed files with 662 additions and 184 deletions

View file

@ -3,22 +3,21 @@ kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: KeyCape verifier custody for the secrets-engine-approval confidential client
status: proposed
status: approved
created: '2026-09-08'
updated: '2026-09-09'
updated: '2026-09-08'
requester:
agent: claude
reason: >-
KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260) asks
railiance-platform to admit custody for two confidential client_credentials
registrations. This CCR covers the first: client secrets-engine-approval,
subject service:secrets-engine, audience approval-engine, tenant:platform per
resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime.
KeyCape owns the registration and issuance; this request establishes only the
custody path and the KeyCape-side delivery of the client secret it must verify
presented credentials against. It does not admit the client-side lane by which
secrets-engine would read its own copy — that is a separate request with a
separate consumer gate.
reason: "KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260)\
\ asks railiance-platform to admit custody for two confidential client_credentials\
\ registrations. This CCR covers the first: client secrets-engine-approval, subject\
\ service:secrets-engine, audience approval-engine, tenant:platform per resolved\
\ decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime. KeyCape owns\
\ the registration and issuance; this request establishes only the custody path\
\ and the KeyCape-side delivery of the client secret it must verify presented\
\ credentials against. It does not admit the client-side lane by which secrets-engine\
\ would read its own copy \u2014 that is a separate request with a separate consumer\
\ gate."
review:
required: true
required_approvers:
@ -28,25 +27,36 @@ review:
- at: '2026-09-08'
reviewer: railiance-platform (codex/claude)
decision: paths_confirmed_field_corrected
comment: >-
KV path platform/workloads/secrets-engine/approval-client is confirmed
unchanged: it matches the platform/workloads/<workload>/<lane> convention
enforced by scripts/credential-change.py and used by every admitted platform
lane. The proposed field name client_secret is corrected to CLIENT_SECRET;
KV field names are uppercase by convention (PROVISIONER_TOKEN,
CORE_HUB_API_TOKEN) and lowercase names fail FIELD_NAME_RE validation. The
Kubernetes Secret key client-secret and the KeyCape environment name are
confirmed as proposed.
comment: 'KV path platform/workloads/secrets-engine/approval-client is confirmed
unchanged: it matches the platform/workloads/<workload>/<lane> convention enforced
by scripts/credential-change.py and used by every admitted platform lane. The
proposed field name client_secret is corrected to CLIENT_SECRET; KV field names
are uppercase by convention (PROVISIONER_TOKEN, CORE_HUB_API_TOKEN) and lowercase
names fail FIELD_NAME_RE validation. The Kubernetes Secret key client-secret
and the KeyCape environment name are confirmed as proposed.'
- at: '2026-09-08T23:05:19+00:00'
reviewer: User (platform-operator; explicit session approval)
decision: approved
comment: User answered "I approve, go on." to the explicit two-CCR approval question
naming both required roles. Scope remains verifier-side custody and its attended
rollout, as reviewed in docs/credential-lane-designs/keycape-approval-clients-review.md.
No client-side read or factory spending grant.
- at: '2026-09-08T23:05:19+00:00'
reviewer: User (key-cape-owner; explicit session approval)
decision: approved
comment: User answered "I approve, go on." to the explicit two-CCR approval question
naming both required roles. Scope remains verifier-side custody and its attended
rollout, as reviewed in docs/credential-lane-designs/keycape-approval-clients-review.md.
No client-side read or factory spending grant.
target:
domain: financials
tenant: platform
workload: secrets-engine
environment: production
purpose: >-
Hold the secrets-engine-approval confidential client secret in platform custody
and project it into the KeyCape runtime so KeyCape can verify presented
client_credentials without the value living in Git, a chart value, or a
hand-created Kubernetes Secret.
purpose: Hold the secrets-engine-approval confidential client secret in platform
custody and project it into the KeyCape runtime so KeyCape can verify presented
client_credentials without the value living in Git, a chart value, or a hand-created
Kubernetes Secret.
openbao:
mount: platform
kv_path: platform/workloads/secrets-engine/approval-client
@ -76,94 +86,72 @@ access_frontdoor:
readiness: pending-review
delivery:
surface: external-secrets
target: >-
ClusterSecretStore openbao-keycape-secrets-engine-approval, limited to
target: 'ClusterSecretStore openbao-keycape-secrets-engine-approval, limited to
namespace sso, to ExternalSecret sso/keycape-secrets-engine-approval-client and
Secret sso/keycape-secrets-engine-approval-client with key client-secret.
KeyCape resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a
secretKeyRef, matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed
on image main-153258b. Manifests:
argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml
and keycape-approval-clients.externalsecrets.yaml.
Secret sso/keycape-secrets-engine-approval-client with key client-secret. KeyCape
resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a secretKeyRef,
matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed on image main-153258b.
Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml
and keycape-approval-clients.externalsecrets.yaml.'
risk:
classification: high
notes:
- >-
This is a verifier-side copy. KeyCape holding the client secret lets it
authenticate the client; it does not let KeyCape act as the client, but a leak
of this value allows anyone to present as secrets-engine for approval:read and
approval:consume until the registration is disabled.
- >-
Custody path is under the client's own workload prefix, not KeyCape's, because
the credential belongs to the secrets-engine client identity. The reader
admitted here is the External Secrets Operator on behalf of the sso namespace
only; a future secrets-engine-side read is a separate lane and separate policy.
- >-
The existing keycape-rapp-qonto-client Secret is hand-created and not
ESO-managed. Do not extend this store or ExternalSecret to adopt it; that is a
distinct migration with its own owner.
- >-
The two approval clients are deliberately kept on separate policies, roles and
- This is a verifier-side copy. KeyCape holding the client secret lets it authenticate
the client; it does not let KeyCape act as the client, but a leak of this value
allows anyone to present as secrets-engine for approval:read and approval:consume
until the registration is disabled.
- Custody path is under the client's own workload prefix, not KeyCape's, because
the credential belongs to the secrets-engine client identity. The reader admitted
here is the External Secrets Operator on behalf of the sso namespace only; a future
secrets-engine-side read is a separate lane and separate policy.
- The existing keycape-rapp-qonto-client Secret is hand-created and not ESO-managed.
Do not extend this store or ExternalSecret to adopt it; that is a distinct migration
with its own owner.
- The two approval clients are deliberately kept on separate policies, roles and
stores so the operator client can be revoked without disturbing this one. Their
scope sets differ; a shared policy would erase that boundary.
- >-
Compromise response is KeyCape disabling the client registration plus rotation
of this KV version. An OpenBao token expiry does not invalidate an already
issued client secret.
- Compromise response is KeyCape disabling the client registration plus rotation
of this KV version. An OpenBao token expiry does not invalidate an already issued
client secret.
verification:
positive:
- >-
The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key
client-secret without printing the value.
- >-
The KeyCape build that reads KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
starts and issues a token for subject service:secrets-engine with audience
approval-engine, tenant:platform, 15m lifetime, verified against live JWKS
signature.
- The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key client-secret
without printing the value.
- The KeyCape build that reads KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET starts
and issues a token for subject service:secrets-engine with audience approval-engine,
tenant:platform, 15m lifetime, verified against live JWKS signature.
- Exact claim bindings are asserted by KeyCape without any value or token logged.
negative:
- >-
A namespace outside the approved ClusterSecretStore condition cannot use this
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- >-
A service account outside external-secrets/external-secrets cannot authenticate
- A service account outside external-secrets/external-secrets cannot authenticate
through role external-secrets-keycape-secrets-engine-approval.
- >-
The role cannot read the sibling approval-engine operator-client path, any
parent listing, or any other platform workload path.
- >-
The secrets-engine-approval client is denied scopes outside approval:read and
- The role cannot read the sibling approval-engine operator-client path, any parent
listing, or any other platform workload path.
- The secrets-engine-approval client is denied scopes outside approval:read and
approval:consume, and human identities are denied approval:consume.
activation_conditions:
- >-
KeyCape and railiance-platform agree the single attended rollout window in
docs/credential-lane-designs/keycape-approval-clients.md; the KeyCape image that
reads both environment names is built and pinned but not yet deployed.
- >-
Attended first provision runs only through the governed
openbao-platform-admin-login lane (founder_required, attended OIDC via
netkingdom role=platform-admin), with a unique receipt path and no value in
chat, command arguments or shell history.
- >-
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.
- KeyCape and railiance-platform agree the single attended rollout window in docs/credential-lane-designs/keycape-approval-clients.md;
the KeyCape image that reads both environment names is built and pinned but not
yet deployed.
- Attended first provision runs only through the governed openbao-platform-admin-login
lane (founder_required, attended OIDC via netkingdom role=platform-admin), with
a unique receipt path and no value in chat, command arguments or shell history.
- 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: []
lifecycle:
deactivate: >-
KeyCape disables the secrets-engine-approval registration; platform detaches
the policy from role external-secrets-keycape-secrets-engine-approval and
removes the ExternalSecret. The KV version is retained until KeyCape confirms
deactivate: KeyCape disables the secrets-engine-approval registration; platform
detaches the policy from role external-secrets-keycape-secrets-engine-approval
and removes the ExternalSecret. The KV version is retained until KeyCape confirms
disablement.
rotate: >-
KeyCape mints a replacement client secret; platform writes the new KV version
rotate: KeyCape mints a replacement client secret; platform writes the new KV version
under the same attended authority; ESO refresh delivers it and KeyCape restarts
or re-reads. Rotation is independent of the operator client.
compromised: >-
Disable the registration at KeyCape first (that is what stops token issuance),
then rotate the KV version and confirm no other namespace consumed the store.
compromised: Disable the registration at KeyCape first (that is what stops token
issuance), then rotate the KV version and confirm no other namespace consumed
the store.
state_hub:
decision_id: b533a271-b704-4c5c-98a2-9a5951aadfb6
decision_api_url: http://127.0.0.1:8000/decisions/b533a271-b704-4c5c-98a2-9a5951aadfb6

View file

@ -3,19 +3,17 @@ kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: KeyCape verifier custody for the approval-engine-operator confidential client
status: proposed
status: approved
created: '2026-09-08'
updated: '2026-09-09'
updated: '2026-09-08'
requester:
agent: claude
reason: >-
Second of the two registrations in KEY-WP-0013-T02 (State Hub message
278a3ebe-b529-49f6-bd1a-e3ebcf318260): client approval-engine-operator, subject
service:approval-engine-operator, audience approval-engine, tenant:platform per
resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime.
Scopes are approval:create, read, approve, revoke, supersede, observe and emit,
explicitly without approval:consume. KeyCape owns registration and issuance;
this request establishes only custody and the KeyCape-side delivery.
reason: 'Second of the two registrations in KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260):
client approval-engine-operator, subject service:approval-engine-operator, audience
approval-engine, tenant:platform per resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6,
15m token lifetime. Scopes are approval:create, read, approve, revoke, supersede,
observe and emit, explicitly without approval:consume. KeyCape owns registration
and issuance; this request establishes only custody and the KeyCape-side delivery.'
review:
required: true
required_approvers:
@ -25,20 +23,31 @@ review:
- at: '2026-09-08'
reviewer: railiance-platform (codex/claude)
decision: paths_confirmed_field_corrected
comment: >-
KV path platform/workloads/approval-engine/operator-client is confirmed
unchanged and conforms to the platform/workloads/<workload>/<lane>
convention. Field client_secret is corrected to CLIENT_SECRET for the same
uppercase convention and validator constraint as CCR-2026-0017. Kubernetes
Secret sso/keycape-approval-engine-operator-client with key client-secret and
the KeyCape environment name are confirmed as proposed.
comment: KV path platform/workloads/approval-engine/operator-client is confirmed
unchanged and conforms to the platform/workloads/<workload>/<lane> convention.
Field client_secret is corrected to CLIENT_SECRET for the same uppercase convention
and validator constraint as CCR-2026-0017. Kubernetes Secret sso/keycape-approval-engine-operator-client
with key client-secret and the KeyCape environment name are confirmed as proposed.
- at: '2026-09-08T23:05:20+00:00'
reviewer: User (platform-operator; explicit session approval)
decision: approved
comment: User answered "I approve, go on." to the explicit two-CCR approval question
naming both required roles. Scope remains verifier-side custody and its attended
rollout, as reviewed in docs/credential-lane-designs/keycape-approval-clients-review.md.
No client-side read or factory spending grant.
- at: '2026-09-08T23:05:20+00:00'
reviewer: User (key-cape-owner; explicit session approval)
decision: approved
comment: User answered "I approve, go on." to the explicit two-CCR approval question
naming both required roles. Scope remains verifier-side custody and its attended
rollout, as reviewed in docs/credential-lane-designs/keycape-approval-clients-review.md.
No client-side read or factory spending grant.
target:
domain: financials
tenant: platform
workload: approval-engine
environment: production
purpose: >-
Hold the approval-engine-operator confidential client secret in platform
purpose: Hold the approval-engine-operator confidential client secret in platform
custody and project it into the KeyCape runtime so KeyCape can verify presented
client_credentials for the operator client.
openbao:
@ -70,77 +79,58 @@ access_frontdoor:
readiness: pending-review
delivery:
surface: external-secrets
target: >-
ClusterSecretStore openbao-keycape-approval-engine-operator, limited to
target: 'ClusterSecretStore openbao-keycape-approval-engine-operator, limited to
namespace sso, to ExternalSecret sso/keycape-approval-engine-operator-client and
Secret sso/keycape-approval-engine-operator-client with key client-secret.
KeyCape resolves it as KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET through a
secretKeyRef. Manifests:
argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml
and keycape-approval-clients.externalsecrets.yaml.
Secret sso/keycape-approval-engine-operator-client with key client-secret. KeyCape
resolves it as KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET through a secretKeyRef.
Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml
and keycape-approval-clients.externalsecrets.yaml.'
risk:
classification: high
notes:
- >-
This client carries the widest approval scope set in the pair — create,
approve, revoke and supersede. A leak allows forging approval lifecycle actions,
which is a stronger outcome than the read/consume client in CCR-2026-0017.
- >-
The absence of approval:consume is a deliberate separation of duties. Any later
- "This client carries the widest approval scope set in the pair \u2014 create,\
\ approve, revoke and supersede. A leak allows forging approval lifecycle actions,\
\ which is a stronger outcome than the read/consume client in CCR-2026-0017."
- The absence of approval:consume is a deliberate separation of duties. Any later
request to add consume to this client is a new lane decision, not a scope edit.
- >-
Kept on its own policy, role and store so it can be revoked independently of the
- Kept on its own policy, role and store so it can be revoked independently of the
secrets-engine-approval client.
- >-
Compromise response is KeyCape disabling the registration plus rotation of this
- Compromise response is KeyCape disabling the registration plus rotation of this
KV version; approval actions already emitted are not retracted by rotation and
need approval-engine's own audit review.
verification:
positive:
- >-
The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key
client-secret without printing the value.
- >-
The KeyCape build issues a token for subject service:approval-engine-operator
with audience approval-engine, tenant:platform and 15m lifetime, verified
against live JWKS signature.
- The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key client-secret
without printing the value.
- The KeyCape build issues a token for subject service:approval-engine-operator
with audience approval-engine, tenant:platform and 15m lifetime, verified against
live JWKS signature.
negative:
- >-
A namespace outside the approved ClusterSecretStore condition cannot use this
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- >-
A service account outside external-secrets/external-secrets cannot authenticate
- A service account outside external-secrets/external-secrets cannot authenticate
through role external-secrets-keycape-approval-engine-operator.
- >-
The role cannot read the sibling secrets-engine approval-client path, any parent
- The role cannot read the sibling secrets-engine approval-client path, any parent
listing, or any other platform workload path.
- >-
The operator client is denied approval:consume, and denial is observed rather
- The operator client is denied approval:consume, and denial is observed rather
than assumed.
activation_conditions:
- >-
Same single attended rollout window and prepared-but-undeployed KeyCape image as
CCR-2026-0017; see docs/credential-lane-designs/keycape-approval-clients.md.
- >-
Attended first provision only through openbao-platform-admin-login
(founder_required, attended OIDC via netkingdom role=platform-admin) with a
unique receipt path.
- >-
Policy, Kubernetes auth role and ClusterSecretStore applied before the
ExternalSecret; sync confirmed before the KeyCape rollout.
- Same single attended rollout window and prepared-but-undeployed KeyCape image
as CCR-2026-0017; see docs/credential-lane-designs/keycape-approval-clients.md.
- Attended first provision only through openbao-platform-admin-login (founder_required,
attended OIDC via netkingdom role=platform-admin) with a unique receipt path.
- 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: []
lifecycle:
deactivate: >-
KeyCape disables the approval-engine-operator registration; platform detaches the
policy from role external-secrets-keycape-approval-engine-operator and removes
the ExternalSecret.
rotate: >-
KeyCape mints a replacement client secret; platform writes the new KV version
deactivate: KeyCape disables the approval-engine-operator registration; platform
detaches the policy from role external-secrets-keycape-approval-engine-operator
and removes the ExternalSecret.
rotate: KeyCape mints a replacement client secret; platform writes the new KV version
under the same attended authority. Rotation is independent of CCR-2026-0017.
compromised: >-
Disable the registration at KeyCape, rotate the KV version, and refer emitted
approval actions to approval-engine for audit review.
compromised: Disable the registration at KeyCape, rotate the KV version, and refer
emitted approval actions to approval-engine for audit review.
state_hub:
decision_id: efa90517-0cae-4eb6-a68d-5b0489c84d65
decision_api_url: http://127.0.0.1:8000/decisions/efa90517-0cae-4eb6-a68d-5b0489c84d65

View file

@ -1,21 +1,22 @@
# KeyCape approval-client custody: review packet
Prepared 2026-09-09 by the-custodian. Both requests are **proposed**; no named
approval or custody mutation has been recorded. This is the review gate for
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.
| Request | Secret path and field | Client authority | Pending decision |
| 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` |
Both require **platform-operator and key-cape-owner** review. Each has its own
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 approved object would authorize verifier-side custody only. Client-side
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.
@ -29,14 +30,16 @@ 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 generated
plan leaves initial secret provisioning to the contained owner procedure; it is
not an already exercised end-to-end activation command. Finish and exercise that
procedure before opening the activation window. Current proposed requests refuse
`apply-plan`; no status has been changed to get around that refusal.
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.
Review outcome: approve, deny or needs changes, naming the reviewer and the role
they are accepting. Both required role confirmations must be recorded for each
CCR before execution. A generic CLI status change alone is insufficient evidence
that both reviews occurred. No owner messages or browser login were triggered
by preparation of this packet.
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.

View file

@ -0,0 +1,36 @@
{
"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"
]
}
]
}

View file

@ -123,3 +123,9 @@ path "platform/data/workloads/telemetry/grafana-admin" {
path "platform/metadata/workloads/telemetry/grafana-admin" {
capabilities = ["deny"]
}
# Approved verifier custody: CCR-2026-0017 / CCR-2026-0018.
path "platform/data/workloads/secrets-engine/approval-client" { capabilities = ["deny"] }
path "platform/data/workloads/approval-engine/operator-client" { capabilities = ["deny"] }
path "platform/metadata/workloads/secrets-engine/approval-client" { capabilities = ["deny"] }
path "platform/metadata/workloads/approval-engine/operator-client" { capabilities = ["deny"] }

View file

@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""Silent, attended first provision and verification of CCR-2026-0017/0018."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import secrets
import time
from datetime import datetime, timezone
import yaml
from state_hub_preflight_lane import ROOT, LaneError, assert_cluster, bao, command, data, revoke
LANES = (
('CCR-2026-0017', 'secrets-engine', 'approval-client', 'keycape-secrets-engine-approval'),
('CCR-2026-0018', 'approval-engine', 'operator-client', 'keycape-approval-engine-operator'),
)
BOUNDARY = 'sys/policies/acl/agent-high-risk-boundary'
def require(condition, reason):
if not condition:
raise LaneError(reason)
def contracts():
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)
result = []
for ref, workload, purpose, suffix in LANES:
path = module.resolve_ccr(ref)
ccr, errors, _ = module.validate_ccr(path)
require(not errors and ccr['status'] in {'approved', 'applied', 'verified', 'active'}, 'approved_ccrs_required')
for role in ('platform-operator', 'key-cape-owner'):
require(any(x.get('decision') == 'approved' and role in x.get('reviewer', '')
and 'explicit session approval' in x.get('reviewer', '')
for x in ccr['review']['comments']), 'both_named_reviews_required')
expected_path = f'platform/workloads/{workload}/{purpose}'
policy = 'workload-kv-read-' + suffix
role = 'external-secrets-' + suffix
expected_role = {'bound_service_account_names': ['external-secrets'],
'bound_service_account_namespaces': ['external-secrets'],
'policies': policy, 'ttl': '15m'}
require(ccr['openbao']['kv_path'] == expected_path and ccr['openbao']['fields'] == ['CLIENT_SECRET']
and ccr['openbao']['policy_name'] == policy
and ccr['openbao']['auth']['role'] == role
and ccr['openbao']['auth']['mount'] == 'kubernetes'
and ccr['openbao']['auth']['bound_claims_confirmed'] is True
and module.auth_payload(ccr) == expected_role, 'exact_approved_contract_required')
hcl = module.generated_policy_hcl(ccr)
require((ROOT / ccr['openbao']['policy_file']).read_text() == hcl, 'policy_source_drift')
result.append({'ccr': ref, 'kv': expected_path.replace('platform/', 'platform/data/', 1),
'metadata': expected_path.replace('platform/', 'platform/metadata/', 1),
'policy': policy, 'hcl': hcl, 'role': role, 'role_payload': expected_role,
'store': 'openbao-' + suffix, 'secret': suffix + '-client',
'source_sha256': hashlib.sha256(path.read_bytes()).hexdigest()})
return result
def read_optional(path):
result = bao(['read', '-format=json', path], allow_failure=True)
if result.returncode == 0:
return data(result)['data']
require(b'404' in result.stderr or b'No value found' in result.stderr, 'absence_not_proven')
return None
def role_matches(actual, lane):
return (actual.get('bound_service_account_names') == ['external-secrets']
and actual.get('bound_service_account_namespaces') == ['external-secrets']
and not actual.get('bound_service_account_namespace_selector')
and actual.get('token_policies') == [lane['policy']]
and actual.get('token_ttl') == 900 and not actual.get('token_period')
and not actual.get('audience') and not actual.get('token_no_default_policy')
and not actual.get('token_bound_cidrs') and not actual.get('token_num_uses'))
def provision(lanes, receipt):
# Complete all drift/absence checks before the first write.
for lane in lanes:
require(read_optional(lane['metadata']) is None, 'existing_custody_requires_resume_or_rotation_review')
old_policy = read_optional('sys/policies/acl/' + lane['policy'])
require(old_policy is None or old_policy['policy'] == lane['hcl'], 'policy_drift')
old_role = read_optional('auth/kubernetes/role/' + lane['role'])
require(old_role is None or role_matches(old_role, lane), 'role_drift')
# The new values must not become readable through a generic coding-agent grant.
boundary = read_optional(BOUNDARY)
require(boundary is not None, 'coding_agent_boundary_required')
current = boundary['policy']
additions = ''
for lane in lanes:
for path in (lane['kv'], lane['metadata']):
if '"' + path + '"' not in current:
additions += 'path "' + path + '" { capabilities = ["deny"] }\n'
else:
require(re.search(r'path\s+"' + re.escape(path) + r'"\s*\{\s*capabilities\s*=\s*\["deny"\]\s*\}', current), 'boundary_path_drift')
if additions:
require(read_optional(BOUNDARY)['policy'] == current, 'boundary_revision_changed')
bao(['write', BOUNDARY, '-'], payload={'policy': current + '\n' + additions})
require(read_optional(BOUNDARY)['policy'] == current + '\n' + additions, 'boundary_readback_failed')
receipt['coding_agent_boundary_extended_only_to_new_paths'] = bool(additions)
for lane in lanes:
row = {'ccr': lane['ccr'], 'source_sha256': lane['source_sha256'], 'policy_applied': False,
'role_applied': False, 'custody_seeded': False}
receipt['lanes'].append(row)
bao(['write', 'sys/policies/acl/' + lane['policy'], '-'], payload={'policy': lane['hcl']})
row['policy_applied'] = True
bao(['write', 'auth/kubernetes/role/' + lane['role'], '-'], payload=lane['role_payload'])
row['role_applied'] = True
require(read_optional('sys/policies/acl/' + lane['policy'])['policy'] == lane['hcl'], 'policy_readback_failed')
require(role_matches(read_optional('auth/kubernetes/role/' + lane['role']), lane), 'role_readback_failed')
for lane, row in zip(lanes, receipt['lanes']):
result = data(bao(['write', '-format=json', lane['kv'], '-'],
payload={'options': {'cas': 0}, 'data': {'CLIENT_SECRET': secrets.token_urlsafe(48)}}))
require(result['data']['version'] == 1, 'unexpected_initial_version')
row.update(custody_seeded=True, kv_version=1, request_id=result.get('request_id'))
receipt['initial_values_generation'] = 'independent CSPRNG 48-byte values; memory-to-OpenBao stdin only; CAS=0'
def require_denied(result):
require(result.returncode != 0 and b'403' in result.stderr and b'permission denied' in result.stderr.lower(), 'denial_not_proven')
def verify_native(kube, lanes, receipt):
for lane, row in zip(lanes, receipt['lanes']):
jwt = command(kube + ['-n', 'external-secrets', 'create', 'token', 'external-secrets', '--duration=10m']).stdout.decode().strip()
auth = data(bao(['write', '-format=json', 'auth/kubernetes/login', '-'], payload={'role': lane['role'], 'jwt': jwt}))['auth']
token = auth['client_token']
try:
require(set(auth['token_policies']) == {lane['policy'], 'default'} and 0 < auth['lease_duration'] <= 900, 'issued_role_or_ttl_mismatch')
native = data(bao(['read', '-format=json', lane['kv']], token=token))['data']
require(set(native['data']) == {'CLIENT_SECRET'} and len(native['data']['CLIENT_SECRET']) == 64, 'custody_shape_mismatch')
sibling = next(x['kv'] for x in lanes if x['ccr'] != lane['ccr'])
require_denied(bao(['read', '-format=json', sibling], token=token, allow_failure=True))
require_denied(bao(['list', '-format=json', 'platform/metadata/workloads'], token=token, allow_failure=True))
row.update(native_reader_verified=True, cross_path_denied=True, parent_listing_denied=True, auth_ttl=auth['lease_duration'])
finally:
revoke(token)
require_denied(bao(['token', 'lookup', '-format=json'], token=token, allow_failure=True))
row['reader_revocation_verified'] = True
bad_jwt = command(kube + ['-n', 'external-secrets', 'create', 'token', 'default', '--duration=10m']).stdout.decode().strip()
bad = bao(['write', '-format=json', 'auth/kubernetes/login', '-'], payload={'role': lane['role'], 'jwt': bad_jwt}, allow_failure=True)
if bad.returncode == 0:
revoke(data(bad)['auth']['client_token'])
raise LaneError('wrong_service_account_authenticated')
require(b'403' in bad.stderr and b'service account' in bad.stderr.lower(), 'wrong_service_account_denial_inconclusive')
row['wrong_service_account_denied'] = True
boundary_child = data(bao(['token', 'create', '-format=json', '-policy=' + lane['policy'], '-policy=agent-high-risk-boundary', '-no-default-policy', '-ttl=60s']))['auth']['client_token']
try:
for path in (lane['kv'], lane['metadata']):
require_denied(bao(['read', '-format=json', path], token=boundary_child, allow_failure=True))
row['coding_agent_deny_wins'] = True
finally:
revoke(boundary_child)
def deliver(kube, lanes, receipt):
base = ROOT / 'argocd/platform-addons/openbao-secretstore'
command(kube + ['apply', '-f', str(base / 'openbao-keycape-approval-clients.clustersecretstore.yaml')])
for lane in lanes:
command(kube + ['wait', '--for=condition=Ready', 'clustersecretstore/' + lane['store'], '--timeout=45s'])
command(kube + ['apply', '-f', str(base / 'keycape-approval-clients.externalsecrets.yaml')])
for lane, row in zip(lanes, receipt['lanes']):
command(kube + ['-n', 'sso', 'wait', '--for=condition=Ready', 'externalsecret/' + lane['secret'], '--timeout=45s'])
es = data(command(kube + ['-n', 'sso', 'get', 'externalsecret', lane['secret'], '-o', 'json']))
secret = data(command(kube + ['-n', 'sso', 'get', 'secret', lane['secret'], '-o', 'json']))
import base64
native = data(bao(['read', '-format=json', lane['kv']]))['data']
require(set(secret['data']) == {'client-secret'} and base64.b64decode(secret['data']['client-secret']).decode() == native['data']['CLIENT_SECRET'], 'eso_delivery_mismatch')
require(any(x['uid'] == es['metadata']['uid'] for x in secret['metadata'].get('ownerReferences', [])), 'eso_does_not_own_secret')
row.update(store_ready=True, external_secret_ready=True, delivery_matches=True,
secret_uid=secret['metadata']['uid'], secret_resource_version=secret['metadata']['resourceVersion'])
def verify_namespace_boundaries(kube, lanes, receipt):
# A unique, exclusively created namespace isolates the negative probes.
ns = 'keycape-custody-check-' + secrets.token_hex(5)
created = data(command(kube + ['create', 'namespace', ns, '-o', 'json']))
try:
command(kube + ['-n', ns, 'create', 'serviceaccount', 'external-secrets'])
jwt = command(kube + ['-n', ns, 'create', 'token', 'external-secrets', '--duration=10m']).stdout.decode().strip()
for lane, row in zip(lanes, receipt['lanes']):
bad = bao(['write', '-format=json', 'auth/kubernetes/login', '-'],
payload={'role': lane['role'], 'jwt': jwt}, allow_failure=True)
if bad.returncode == 0:
revoke(data(bad)['auth']['client_token'])
raise LaneError('wrong_namespace_authenticated')
require(b'403' in bad.stderr and b'namespace' in bad.stderr.lower(), 'wrong_namespace_denial_inconclusive')
row['wrong_namespace_denied'] = True
probe = {'apiVersion': 'external-secrets.io/v1', 'kind': 'ExternalSecret',
'metadata': {'name': lane['secret'], 'namespace': ns},
'spec': {'refreshInterval': '15s',
'secretStoreRef': {'kind': 'ClusterSecretStore', 'name': lane['store']},
'target': {'name': lane['secret'], 'creationPolicy': 'Owner'},
'data': [{'secretKey': 'client-secret', 'remoteRef': {
'key': lane['kv'].removeprefix('platform/data/'), 'property': 'CLIENT_SECRET'}}]}}
command(kube + ['create', '-f', '-'], payload=probe)
denied = False
for _ in range(20):
result = data(command(kube + ['-n', ns, 'get', 'externalsecret', lane['secret'], '-o', 'json']))
conditions = result.get('status', {}).get('conditions', [])
denied = any(c.get('type') == 'Ready' and c.get('status') == 'False'
and c.get('reason') == 'SecretSyncedError' for c in conditions)
if denied:
events = data(command(kube + ['-n', ns, 'get', 'events',
'--field-selector=involvedObject.uid=' + result['metadata']['uid'], '-o', 'json']))
denied = any('not allowed' in e.get('message', '').lower()
and 'namespace' in e.get('message', '').lower() for e in events['items'])
if denied:
break
time.sleep(2)
require(denied, 'outside_namespace_store_denial_inconclusive')
absent = command(kube + ['-n', ns, 'get', 'secret', lane['secret'], '-o', 'json'], allow_failure=True)
require(absent.returncode != 0 and b'NotFound' in absent.stderr, 'outside_namespace_secret_not_absent')
row['outside_namespace_store_denied'] = True
finally:
command(kube + ['delete', '--raw=/api/v1/namespaces/' + ns, '-f', '/dev/stdin'],
payload={'apiVersion': 'v1', 'kind': 'DeleteOptions',
'preconditions': {'uid': created['metadata']['uid']}})
receipt['namespace_probe_cleanup_requested'] = True
def load_rollout():
path = Path('/home/worsch/net-kingdom/sso-mfa/k8s/keycape/approval-clients-rollout.py')
spec = importlib.util.spec_from_file_location('keycape_rollout', path)
module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
return module
def deactivate_delivery(kube, lanes, receipt):
# Config/image restoration runs in the owner helper before this call.
for lane in lanes:
existing = command(kube + ['-n', 'sso', 'get', 'externalsecret', lane['secret'], '-o', 'json'], allow_failure=True)
if existing.returncode == 0:
obj = data(existing)
require(obj['spec']['secretStoreRef'] == {'kind': 'ClusterSecretStore', 'name': lane['store']}, 'rollback_external_secret_drift')
command(kube + ['delete', '--raw=/apis/external-secrets.io/v1/namespaces/sso/externalsecrets/' + lane['secret'], '-f', '/dev/stdin'],
payload={'apiVersion': 'v1', 'kind': 'DeleteOptions', 'preconditions': {'uid': obj['metadata']['uid']}})
else:
require(b'NotFound' in existing.stderr, 'rollback_external_secret_absence_unproven')
role = read_optional('auth/kubernetes/role/' + lane['role'])
if role is not None:
require(role_matches(role, lane), 'rollback_auth_role_drift')
payload = dict(lane['role_payload'], policies=[])
bao(['write', 'auth/kubernetes/role/' + lane['role'], '-'], payload=payload)
receipt['verifier_delivery_disabled_custody_versions_retained'] = True
def run(args, receipt):
require(Path.home().parent.name == '.warden-attended-login' and not os.environ.get('BAO_TOKEN') and not os.environ.get('VAULT_TOKEN'), 'attended_warden_envelope_required')
lanes = contracts()
kube = ['kubectl', '--kubeconfig', args.kubeconfig]
assert_cluster(kube)
identity = data(bao(['token', 'lookup', '-format=json']))['data']
require('platform-admin' in identity['policies'] and 'root' not in identity['policies'], 'attended_platform_admin_required')
env = os.environ.copy(); env['KUBECONFIG'] = args.kubeconfig
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
if rollout:
# Exercise config construction and API admission before any custody write.
secret = rollout.get(kube, 'secret', 'keycape-config')
dep = rollout.get(kube, 'deployment', 'keycape')
config = rollout.replacement(secret, rollout.registrations())
candidate = rollout.candidate_spec(dep)
rollout.patch_object(kube, 'secret', secret, '/data/config.yaml', config, dry=True)
rollout.patch_object(kube, 'deployment', dep, '/spec', candidate, dry=True)
require(args.recovery and not Path(args.recovery).exists(), 'unique_recovery_path_required')
for lane in lanes:
for kind in ('secret', 'externalsecret'):
existing = command(kube + ['-n', 'sso', 'get', kind, lane['secret'], '-o', 'json'], allow_failure=True)
require(existing.returncode != 0 and b'NotFound' in existing.stderr, 'existing_delivery_requires_reconciliation')
try:
receipt['phase'] = 'provision'
if args.action in {'provision', 'activate'}:
provision(lanes, receipt)
else:
receipt['lanes'] = [{'ccr': lane['ccr']} for lane in lanes]
receipt['phase'] = 'native_verification'
verify_native(kube, lanes, receipt)
receipt['phase'] = 'eso_delivery'
deliver(kube, lanes, receipt)
receipt['phase'] = 'namespace_boundaries'
verify_namespace_boundaries(kube, lanes, receipt)
receipt['status'] = 'custody_and_eso_verified_pending_keycape_rollout'
if rollout:
receipt['phase'] = 'keycape_rollout'
receipt['keycape'] = {'existing_human_login_before': True}
rollout.rollout(kube, receipt['keycape'], Path(args.recovery))
receipt['status'] = 'custody_and_service_acceptance_passed_pending_fresh_human_login'
receipt['keycape_rollout_completed'] = True
receipt['phase'] = 'awaiting_fresh_human_login'
except Exception:
receipt['status'] = 'failed'
if rollout and (not receipt.get('keycape') or receipt['keycape'].get('compatible_pair_restored')):
deactivate_delivery(kube, lanes, receipt)
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('action', choices=['provision', 'verify', 'activate'])
parser.add_argument('--kubeconfig', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--recovery')
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,
'client_side_read_admitted': False, 'keycape_rollout_completed': 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['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())

View file

@ -0,0 +1,114 @@
"""Opt-in local OpenBao exercise; no production API or credential helper used."""
import importlib.util
import json
import os
from pathlib import Path
import secrets
import shutil
import socket
import subprocess
import sys
import time
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'scripts'))
import keycape_approval_custody as lane
class CustodyExercise(unittest.TestCase):
@classmethod
def setUpClass(cls):
if not shutil.which('bao'):
raise unittest.SkipTest('OpenBao executable required')
sock = socket.socket(); sock.bind(('127.0.0.1', 0)); port = sock.getsockname()[1]; sock.close()
cls.env = os.environ.copy()
token = secrets.token_urlsafe(32)
for name in ('BAO_ADDR', 'VAULT_ADDR'):
cls.env[name] = f'http://127.0.0.1:{port}'
for name in ('BAO_TOKEN', 'VAULT_TOKEN', 'BAO_DEV_ROOT_TOKEN_ID'):
cls.env[name] = token
cls.server = subprocess.Popen(['bao', 'server', '-dev', '-dev-no-store-token',
f'-dev-listen-address=127.0.0.1:{port}'], env=cls.env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
cls.environment = patch.dict(os.environ, cls.env, clear=True); cls.environment.start()
try:
for _ in range(40):
if lane.bao(['status'], allow_failure=True).returncode == 0:
break
time.sleep(.25)
else:
raise RuntimeError('local_dev_server_not_ready')
lane.bao(['secrets', 'enable', '-path=platform', 'kv-v2'])
lane.bao(['auth', 'enable', 'kubernetes'])
cls.lanes = lane.contracts()
except Exception:
cls.environment.stop(); cls.server.terminate(); cls.server.wait(timeout=10)
raise
@classmethod
def tearDownClass(cls):
cls.environment.stop(); cls.server.terminate(); cls.server.wait(timeout=10)
def test_01_first_provision_and_real_acl_boundaries(self):
lane.bao(['write', lane.BOUNDARY, '-'], payload={'policy': 'path "protected/fixture" { capabilities = ["deny"] }\n'})
receipt = {'lanes': []}
lane.provision(self.lanes, receipt)
values = []
for item in self.lanes:
value = lane.data(lane.bao(['read', '-format=json', item['kv']]))['data']['data']['CLIENT_SECRET']
self.assertTrue(len(value) == 64)
values.append(value)
auth = lane.data(lane.bao(['token', 'create', '-format=json', '-policy=' + item['policy'], '-ttl=60s']))['auth']
token = auth['client_token']
try:
self.assertEqual(lane.bao(['read', '-format=json', item['kv']], token=token).returncode, 0)
sibling = next(x['kv'] for x in self.lanes if x != item)
lane.require_denied(lane.bao(['read', sibling], token=token, allow_failure=True))
lane.require_denied(lane.bao(['list', 'platform/metadata/workloads'], token=token, allow_failure=True))
finally:
lane.revoke(token)
lane.require_denied(lane.bao(['token', 'lookup'], token=token, allow_failure=True))
child = lane.data(lane.bao(['token', 'create', '-format=json', '-policy=' + item['policy'], '-policy=agent-high-risk-boundary', '-ttl=60s']))['auth']['client_token']
try:
lane.require_denied(lane.bao(['read', item['kv']], token=child, allow_failure=True))
finally:
lane.revoke(child)
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))
def test_02_retry_refuses_existing_custody(self):
with self.assertRaisesRegex(lane.LaneError, 'existing_custody_requires'):
lane.provision(self.lanes, {'lanes': []})
self.assertTrue(all(lane.read_optional(x['metadata'])['current_version'] == 1 for x in self.lanes))
def test_03_denials_must_be_authoritative(self):
for result in [subprocess.CompletedProcess([], 0, b'', b''),
subprocess.CompletedProcess([], 1, b'', b'404 Not Found'),
subprocess.CompletedProcess([], 1, b'', b'503 permission denied'),
subprocess.CompletedProcess([], 1, b'', b'403 unrelated refusal')]:
with self.assertRaises(lane.LaneError):
lane.require_denied(result)
def test_04_missing_approval_refused(self):
with patch.object(lane, 'ROOT', Path('/nonexistent-keycape-contract')):
with self.assertRaises(FileNotFoundError):
lane.contracts()
def test_05_role_drift_refused(self):
item = self.lanes[0]
actual = lane.read_optional('auth/kubernetes/role/' + item['role'])
self.assertTrue(lane.role_matches(actual, item))
for field, value in [('token_policies', [item['policy'], 'extra']),
('bound_service_account_namespaces', ['*']),
('bound_service_account_names', ['*']), ('token_ttl', 3600), ('token_period', 900)]:
self.assertFalse(lane.role_matches(dict(actual, **{field: value}), item))
def test_06_direct_execution_refused(self):
with self.assertRaisesRegex(lane.LaneError, 'attended_warden_envelope_required'):
lane.run(None, {})
if __name__ == '__main__':
unittest.main()

View file

@ -145,9 +145,9 @@ closure: `history/2026-09-05-preflight-signing-activation-complete.md`.
```task
id: RPF-WP-0035-T05
status: wait
needs_human: true
intervention_note: "Review CCR-2026-0017 and CCR-2026-0018 as platform-operator and key-cape-owner; both pending decisions are linked from the CCRs and the review packet. Signed issuer and exact configuration pin are proved."
status: progress
needs_human: false
intervention_note: ""
priority: high
state_hub_task_id: "e15d62c9-e5da-5721-a135-87c050f7851c"
```
@ -218,6 +218,16 @@ human dependency; HFACT consumes it without a duplicate approval request.
Custody/ESO activation, client verification and the separate client-side/audit
lanes remain open. Current CLI apply plans refuse the proposed requests.
2026-09-09 admission: the user explicitly approved both CCRs in the named
platform-operator and key-cape-owner roles. Both source requests are approved
and both linked Hub decisions are resolved. Receipt:
`docs/evidence/2026-09-09-keycape-approval-admission.json`. The completed review
intervention is cleared; T05 is progress while the contained first-provision
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.
## Dependency review — 2026-09-06
SECRETS-WP-0008-T02 now records the local PIP claim/validation join implemented