Revise KeyCape secrets-engine-openbao custody: OpenBao-only, operator reader CCR-2026-0034 (source only)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Founder direction 2026-10-06 and ops-mason plan keycape-secrets-engine-openbao-client-custody
(658cdbc, MASON-WP-0009): the client secret lives only in OpenBao.

- CCR-2026-0033: consumer file and secrets-engine D1-D4 removed; KV metadata declared
  (cas_required, max_versions=2, delete_version_after=0s, custom_metadata); target.domain
  infotech; live order and roll-forward rollback revised; status stays proposed.
- New sibling CCR-2026-0034 + policy: attended OIDC operator reader on auth/netkingdom,
  read data path + revoke-self only, 5m/4 uses, no default policy, bound
  groups=net-kingdom-admins and /assurance/level=aal2 (KeyCape 7d96ee4). Proposed,
  bound claims unconfirmed (founder item for both readers).
- credential-change.py: OIDC roles carry token_max_ttl/explicit_max_ttl/no_default_policy/
  num_uses/type and bound_claims_type (string only); token_revoke_self policy option;
  validated openbao.kv_metadata. Tests added.
- Custody child: consumer write and check-secret-file removed; cas=0 create / --rotate
  cas=<current>; compare-before-write metadata put; refusals unchanged.
- Founder runbook (keycape/founder-runbook.md), window docs, RPF-WP-0035-T09 (waits on A1/A2/A3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 132360@bnt-lap001
Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
This commit is contained in:
codex 2026-10-06 14:26:42 +02:00
parent 9e23eaa067
commit 4a284f0a3b
11 changed files with 734 additions and 307 deletions

View file

@ -47,27 +47,43 @@ the refusal. This is separate from actual provider registration/custody.
The KeyCape packet's candidate path `platform/services/keycape/secrets-engine-openbao`
is outside the platform-mount applier scope (`platform/workloads/` only) and was
corrected on 2026-10-06 to `platform/workloads/keycape/secrets-engine-openbao`,
field CLIENT_SECRET. Consumers bind to the sso Secret and the consumer file, not
the KV path. The attended custody child
`scripts/keycape_secrets_engine_openbao_custody.py` (source only, never run) and
the secrets-engine consumer copy (SECRETS-WP-0012, a3897c2) are recorded in
CCR-2026-0033 with the explicit live order.
Proposed CCR-2026-0033 and input
`docs/credential-lane-designs/keycape-secrets-engine-openbao-issuer-input.yaml`
declare only the issuer reader. Policy
`workload-kv-read-keycape-secrets-engine-openbao-eso` reads exactly that data path.
Role `external-secrets-keycape-secrets-engine-openbao` binds the existing
external-secrets/external-secrets identity, audience openbao, 15-minute
TTL/max/explicit max and no default policy. The proposed store admits only sso;
target Secret keycape-secrets-engine-openbao-client projects client-secret.
Target annotations are explicit. Auth payload, ESO and receipt checklist are
prepared under this window's `keycape/` directory and outside active GitOps.
field CLIENT_SECRET.
CCR-2026-0033 is proposed, claims unconfirmed and frontdoor unresolvable.
No value exists by implication of this draft. The service client custody child
cannot be finalized before the secrets-engine consumer coordinates are accepted:
issuer-side ESO does not supply a host/path/mechanism for the consumer copy.
No guessed runtime directory, automatic token seed or live registration is added.
**Revised 2026-10-06 (founder direction: the secret lives only in OpenBao; ops-mason
plan `keycape-secrets-engine-openbao-client-custody`, 658cdbc, MASON-WP-0009).**
The secrets-engine consumer file and secrets-engine D1-D4 are withdrawn. Two readers:
- Issuer: CCR-2026-0033, unchanged ESO reader. Policy
`workload-kv-read-keycape-secrets-engine-openbao-eso`, Kubernetes role
`external-secrets-keycape-secrets-engine-openbao` (external-secrets/external-secrets,
audience openbao, 15m, no default policy). The store admits sso only; target Secret
`sso/keycape-secrets-engine-openbao-client` key `client-secret`. It must stay on
Kubernetes auth.
- Operator: sibling CCR-2026-0034, OIDC role
`keycape-secrets-engine-openbao-operator-workload-kv-read` on `auth/netkingdom`.
Policy `workload-kv-read-keycape-secrets-engine-openbao-operator` grants read on
the data path and update on `auth/token/revoke-self` only. The token is 5m, 4 uses,
service type, with no default policy. Bound claims are `groups=net-kingdom-admins`
and `/assurance/level=aal2` (KeyCape 7d96ee4: MFA is not pinned on the openbao-admin
client). secrets-engine reads the value into memory per command, with no copy
at rest.
CCR-2026-0033 now declares the KV metadata: `cas_required=true`, `max_versions=2`,
`delete_version_after=0s`, and custom_metadata as in the plan. Its target.domain is
corrected to infotech. The custody child
`scripts/keycape_secrets_engine_openbao_custody.py` (source only, never run) writes
KV with `cas=0`, or with `cas=<current>` under `--rotate`, then compares the
metadata before it writes. It writes no consumer file. Both CCRs remain proposed with
bound claims unconfirmed. Confirming them is a founder item for both readers.
Founder approvals: A1 design, A2 both CCRs, A3 custody-child decision id (not
632a8893). Live order: custody child → platform applies policies/roles/ESO (Secret
sso/keycape-secrets-engine-openbao-client exists) → NetKingdom env patch (NK-WP-0046)
→ KeyCape registration merge (KEY-WP-0037-T03) → KeyCape restart immediately → live
issuance check (KEY-WP-0037-T04) → OpenBao JWT role/login proof (RPF-WP-0035-T02).
Exact founder commands, including the metadata-only survey:
`keycape/founder-runbook.md`. Payloads and ESO stay outside active GitOps in this
window's `keycape/` directory. No value exists by implication of these drafts.
## Dependency boundary

View file

@ -0,0 +1,155 @@
# Founder runbook: KeyCape `secrets-engine-openbao` client secret custody
Source only. Nothing here has been run. Every command below is run by the
founder, attended, from `/home/worsch/railiance-platform` at the reviewed
commit. No command prints a secret value. Design: ops-mason
`plans/keycape-secrets-engine-openbao-client-custody.md` (658cdbc, MASON-WP-0009).
CCRs: `credential-change-requests/CCR-2026-0033.yaml` (authoritative custody +
issuer ESO reader) and `CCR-2026-0034.yaml` (attended operator OIDC reader).
## Gates before any command
- **A1** approve the ops-mason design.
- **A2** approve CCR-2026-0033 and CCR-2026-0034, each with platform-owner,
key-cape-owner and net-kingdom-owner reviews, and confirm the bound claims of
**both** readers (`bound_claims_confirmed: true`):
- CCR-2026-0033: Kubernetes SA `external-secrets/external-secrets`, audience `openbao`.
- CCR-2026-0034: `groups=net-kingdom-admins` **and** `/assurance/level=aal2`
(`bound_claims_type: string`). KeyCape (7d96ee4) reports that MFA is not pinned on
the openbao-admin client, so this aal2 binding is the lane's MFA guarantee.
- **A3** a founder decision id for the one custody-child run, recorded in an
approved review comment of CCR-2026-0033. It must differ from boundary
decision `632a8893-0d6a-4591-a50b-40bbdb798f32`. Written below as `<A3-decision-id>`.
## Live order
1. Custody child (section B) writes KV v1 plus metadata.
2. Platform applies the policies/roles of both CCRs and the sso-only ESO
(section C). Secret `sso/keycape-secrets-engine-openbao-client` exists.
3. NetKingdom env patch (NK-WP-0046).
4. KeyCape registration merge (KEY-WP-0037-T03).
5. KeyCape restart **immediately** after step 4. Between the KV write and the
restart, exchanges fail closed (401).
6. Live issuance check (KEY-WP-0037-T04).
7. OpenBao JWT role/login proof (RPF-WP-0035-T02).
Steps 3 to 7 belong to their owners and each needs its own go.
## A. Metadata-only survey (read-only; before step 1)
ops-mason's survey generator refuses data paths and runs under the scoped,
value-blind `ops-mason-build` grant:
```bash
cd /home/worsch/ops-mason
./scripts/bao-session.sh grant MASON-WP-0009-T04
./scripts/plan-survey.py --kv-path platform/workloads/keycape/secrets-engine-openbao \
--policy workload-kv-read-keycape-secrets-engine-openbao-operator \
--k8s-role external-secrets-keycape-secrets-engine-openbao \
--consumer secrets-engine --keyword keycape --keyword secrets-engine-openbao
./scripts/bao-session.sh revoke
```
Expected: a Markdown survey block. The block shows KV metadata **absent** at the
path (otherwise the first run must not be used; see `--rotate`). It also shows no
existing role or policy with the two proposed names, and no `auth/jwt` role touching
the path. Paste it into MASON-WP-0009-T04. It contains no value.
## B. Attended custody run (step 1)
Pre-check, offline (no OpenBao call): both CCRs validate and the child is the
reviewed revision.
```bash
python3 scripts/credential-change.py validate credential-change-requests/CCR-2026-0033.yaml
python3 scripts/credential-change.py validate credential-change-requests/CCR-2026-0034.yaml
python3 -m pytest -q tests/test_keycape_secrets_engine_openbao_custody.py
git log -1 --format=%H -- scripts/keycape_secrets_engine_openbao_custody.py
```
Expected: `[OK]` for both with **no** "bound claim is not confirmed" warning;
tests pass. The child has no separate dry-run mode. It refuses before any OpenBao
call when the decision id is missing, wrong or unrecorded, or when either CCR is
not approved and confirmed. It also refuses outside the attended envelope or when
`BAO_TOKEN`/`VAULT_TOKEN` is set.
Run (first write, `cas=0`):
```bash
set +x; umask 077; unset BAO_TOKEN VAULT_TOKEN
warden access openbao-platform-admin-login --exec -- \
python3 scripts/keycape_secrets_engine_openbao_custody.py provision \
--approval-decision <A3-decision-id> \
--receipt /home/worsch/.local/state/railiance/receipts/CCR-2026-0033-custody-$(date -u +%Y%m%dT%H%M%SZ).json
```
Rotation uses the same command with `--rotate` (`cas=<observed current version>`)
under a standing rotation approval for CCR-2026-0033. It is roll-forward only.
Expected: exit 0, no output, and a new mode-0600 receipt (create its parent
directory 0700 first if absent):
```json
{"schema": "platform.keycape-secrets-engine-openbao-custody.v1", "ccr": "CCR-2026-0033",
"status": "custody_written_pending_issuer_delivery", "phase": "done",
"credential_values_emitted": false, "client_side_read_admitted": false,
"approval_decision_id": "<A3-decision-id>", "ccr_sha256": "<digest>",
"kv_written": true, "kv_version": 1, "kv_cas": 0, "kv_request_id": "<id>",
"kv_metadata": "applied", "started_at": "...", "finished_at": "..."}
```
On `status: failed`, read `error` and `phase`. A failure with `kv_written: true`
keeps that version. Recover only with a fresh `--rotate` run, never by reading
the value.
## C. Policies, roles and ESO (step 2)
Policies and roles go through the sanctioned delegated applier. It refuses
unapproved CCRs and unconfirmed bindings, and it diffs the policy files against
the generated bodies.
```bash
# dry-run (offline review of policy diff and role payload, incl. token_num_uses=4, aal2 claim)
python3 scripts/credential-change.py applier-dry-run credential-change-requests/CCR-2026-0033.yaml
python3 scripts/credential-change.py applier-dry-run credential-change-requests/CCR-2026-0034.yaml
python3 scripts/credential-change.py applier-apply credential-change-requests/CCR-2026-0034.yaml --plan-only
# apply (attended platform-admin; one phrase per CCR)
warden access openbao-platform-admin-login --exec -- \
python3 scripts/credential-change.py applier-apply credential-change-requests/CCR-2026-0033.yaml \
--confirm "DELEGATED APPLY CCR-2026-0033" --actor founder --record-state-hub
warden access openbao-platform-admin-login --exec -- \
python3 scripts/credential-change.py applier-apply credential-change-requests/CCR-2026-0034.yaml \
--confirm "DELEGATED APPLY CCR-2026-0034" --actor founder --record-state-hub
```
Expected: `[OK] CCR-2026-0033.yaml delegated metadata apply recorded`, the same for
0034, and a value-free evidence entry in each CCR.
ESO: `eso.draft.yaml` in this directory carries `railiance.io/admission:
pending-review`. Promote it to `approved` in a reviewed commit after A2 first. Then:
```bash
kubectl apply --dry-run=server -f docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml
kubectl apply -f docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml
kubectl -n sso get externalsecret keycape-secrets-engine-openbao-client \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}{"\n"}'
kubectl -n sso get secret keycape-secrets-engine-openbao-client -o name
```
Expected: `ClusterSecretStore ... created (server dry run)` and `ExternalSecret ...
created (server dry run)`, then the real apply, then `True`, then
`secret/keycape-secrets-engine-openbao-client`. Never use another output format
or a template on the Secret.
After step 2, ops-mason runs its capabilities-only verification (MASON-WP-0009-T04).
It checks operator-token read on the data path and deny everywhere else, and that a
non-member or non-aal2 identity cannot obtain the role. It also checks the
circularity invariants and the metadata settings.
## Rollback (roll-forward; never a read of an old value)
Before step 3: delete the CCR-2026-0034 OIDC role, then its policy. Delete the
ExternalSecret (uid precondition), then the ESO role and policy. The KV entry is
kept until a separate destroy approval. After step 3: the NetKingdom rollout
helper restores the prior config/Deployment first, then the same sequence.