railiance-platform/docs/runbooks/flex-auth-envelope-signing-attended-creation.md
codex b3f12b2ffa
All checks were successful
CCR intake gate / gate (push) Successful in 1m9s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
CCR-2026-0050: record access-engine-owner approval; runbook uses Access public-receipt tool and pin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 809600@bnt-lap001
Assistant-Session: 40ba228f-c8a0-4ec9-bb29-3129951afdc4
2026-10-21 01:35:14 +02:00

8.3 KiB

Attended creation: flex-auth decision-envelope signing key (CCR-2026-0050)

Prepared 2026-10-21 under RPF-WP-0035-T14 (founder gate) for the custody route flex-auth-decision-envelope-signing-key. Nothing in this file has been executed. Every step that changes OpenBao or the cluster needs the founder's explicit go for that change, given at the time of the change. No value of the seed is printed, stored in Git, the hub, chat or a log at any point.

Inputs that are fixed by reviewed source:

Item Value
Read CCR credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml
Native action docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json (RPF35-PDP-SIGNER-NATIVE-20261016-R1)
KV document platform/data/workloads/flex-auth/envelope-signing, fields ED25519_PRIVATE_KEY, ED25519_PUBLIC_KEY, kid
kid flex-auth-lifecycle-20261016-r1
Metadata contract cas_required=true, max_versions=2, delete_version_after=0s; bootstrap is CAS 0
Read policy workload-kv-read-flex-auth-envelope-signing (openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl)
Read role auth/kubernetes/role/external-secrets-flex-auth-envelope-signing, SA flex-auth-envelope-signing-eso in flex-auth, audience openbao, 15m, no default policy
ESO projection docs/credential-lane-designs/flex-auth-envelope-signing-native-projection.yaml.proposed (seed property only)
Writer scripts/flex_auth_signer_native.py + scripts/flex_auth_signer_custody.py (sha256:f169ad12…110c, as pinned in the native plan)
Route warden access openbao-platform-admin-login --exec -- <reviewed-command>; routing receipt docs/evidence/2026-10-21-flex-auth-signer-warden-route.json (sha256:ad7d6a1829de53678b008b23627475e7570c71cf50f9f2ad5301635022a7f467, lane openbao-platform-admin-login, verdict founder_required, act oidc_login)
Private receipt dir /home/worsch/.local/state/railiance-platform/flex-auth-envelope-signing (0700, files 0600)

The 2026-10-16 routing blocker (warden selected openbao-structure-survey) no longer reproduces: the 2026-10-21 plan query selects the correct lane.

0. Preconditions (all must hold; any miss = stop)

  1. Five dispositions on CCR-2026-0050 recorded in review.comments: founder, railiance-platform-owner, access-engine-owner, secrets-engine-owner, cluster-runtime-storage-owner (railiance-cluster).
  2. Founder decision on the hub (see RPF-WP-0035-T14) resolved with APPROVE: CCR-2026-0050 ...; then a platform session, not the requester, runs make credential-change-sync-decision CREDENTIAL_CHANGE=CCR-2026-0050.
  3. Access (FLEX-WP-0024-T06) has the retained deployment flex-auth/flex-auth-secrets-engine on the immutable candidate image forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b27…981f stopped: replicas 0, no owned ReplicaSet replicas, no pods using the signer SA or mounting the seed, no HPA. Protected consumers (secrets-engine) fenced. GitOps/Helm writers for the release frozen for the window.
  4. Cluster owner has reviewed the outbox PVC (1Gi local-path) and PV Retain.
  5. Secrets owner is ready to admit the public receipt through secrets-engine/docs/founder-admission.md (SECRETS-WP-0014-T04, decision e4eee965).
  6. Local tree is clean on the reviewed commit; tests pass: uv run python -m pytest -q tests/test_flex_auth_signer_native.py tests/test_flex_auth_signer_custody.py.

1. Read lane: policy and role (founder go #1)

make credential-change-status CREDENTIAL_CHANGE=CCR-2026-0050       # expect status approved
make credential-change-applier-dry-run CREDENTIAL_CHANGE=CCR-2026-0050
make credential-change-applier-apply-plan CREDENTIAL_CHANGE=CCR-2026-0050
# founder go, then:
warden access openbao-platform-admin-login --exec -- \
  python3 scripts/credential-change.py applier-apply CCR-2026-0050 \
    --actor founder --record-state-hub
# the founder types the confirmation phrase the tool prompts for

Then verify native readback of the bound claims (no JWT or Secret printed) and record it: python3 scripts/credential-change.py confirm-binding CCR-2026-0050 --reviewer railiance-platform-owner --comment "<readback summary>".

Rollback: delete role external-secrets-flex-auth-envelope-signing, then policy workload-kv-read-flex-auth-envelope-signing. No KV data exists yet.

Negatives (record with record-evidence): login with a wrong SA, namespace and audience is refused; with the right identity, sibling path read, list, metadata read, write and delete are denied.

2. Seed creation (founder go #2; single attended window)

Do this inside one window of at most 120 seconds after building the admission.

  1. Founder builds the admission file (owner-private, 0600, outside Git) in the schema railiance-platform.signer-native-admission.v1 with action=bootstrap, the kid above, the routing receipt digest above, the immutable image, caller_binding and the five owner_reviews. Each review binds review_scope_digest(admission), which excludes owner_reviews and expires_at, so reviewers can approve the exact scope ahead of the window. Set expires_at to now + 90 s last.
  2. Run, in the founder's terminal only:
warden access openbao-platform-admin-login --exec -- \
  python3 scripts/flex_auth_signer_native.py \
    --admission <abs-path-admission.json> --admission-sha256 sha256:<hex> \
    --receipt-directory /home/worsch/.local/state/railiance-platform/flex-auth-envelope-signing

The child checks the cluster ID, unseal, non-root platform-admin entity, audit enabled, read policy/role readback and the stop fence (rechecked before metadata setup and before CAS). It requires metadata absence (HTTP 404), sets the metadata contract, generates the seed in memory, writes with CAS 0, reads back version 1, and writes a receipt that holds public fields only. It is silent; the outcome is the receipt status:

  • custody-verified-pending-independent-public-admission: success.
  • anything else, or no receipt: unknown. Keep everything fenced. Do not rerun bootstrap. Use the read-only recover action with a fresh admission that binds the original receipt digest.

Rollback before CAS: abort; nothing private was written. After CAS: no restore and no automatic retry. Forward-only recovery under a newly reviewed action.

3. Public receipt for secrets-engine (no private read)

Derive the consumer receipt from the writer receipt, never from a KV read (the KV document also holds the seed and reads are not field-filtered). Use Access's existing tool, which reads public fields only, together with Access's published policy pin (access-engine docs/native-signer-public-pin.json: flex-auth/local, standalone, secrets-engine.catalog-lane.lifecycle v2, sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4):

R=/home/worsch/.local/state/railiance-platform/flex-auth-envelope-signing/<operation_id>.json
python3 ~/access-engine/tools/prepare_signer_public_receipt.py --help   # then run per its usage on "$R"

The result must carry kid, public key, fingerprint, KV version, writer and source/image/deployment hashes and the pin fields (evaluator, mode, policy_package, policy_version, policy_package_digest); these are the two secrets-engine conditions recorded in the CCR. public_key is unpadded base64url, 43 characters. public_fingerprint is sha256: + hex SHA-256 of that ASCII string, not of the raw bytes. The founder delivers the file to the secrets-engine owner over an authenticated channel and signs the consumer trust file per secrets-engine/docs/founder-admission.md.

4. Delivery and signer start (founder go #3, Access-owned)

  1. Access/GitOps applies the reviewed ESO projection (SA, SecretStore, ExternalSecret). Do not use client-side apply on Secrets.
  2. Check readiness with the safe helpers only: scripts/eso_adoption_readiness.py and scripts/secret_annotation_maintenance.py (inspect mode). Never run go-template or jsonpath against the Secret.
  3. Access restarts the single signer replica. Verify kid/source/image, a signed non-effect lifecycle allow and deny, and independent consumer verification before reopening protected consumers.

Negatives: unknown or old kid, wrong policy, tampered, expired or unsigned envelope, wrong caller and missing seed are all refused. No taskpilot enablement.