net-kingdom/sso-mfa/k8s/bounded-executor/T06-PILOT-RUNBOOK.md
tegwick cd265b84a4
All checks were successful
Bounded-execution contracts / contracts (push) Successful in 1m25s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0058-T07: conventions lint, server-side applies, one cluster route
tools/conventions/lint.py (make conventions-lint, contract suite): server-side-apply, one-wait-condition,
one-cluster-route, tool-numbered-receipts, managing-controller (Argo-owned paths from railiance-platform),
with reasoned exceptions. 21 runbook applies made server-side. portal-client-rollout.py (and so
keycape-client.py and the rollout tools) and identity-provisioner-reconcile.py use NK_KUBECONFIG,
default ~/.kube/config-railiance01, without a second kubeconfig or --server override.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 943911@bnt-lap001
Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
2026-11-06 00:34:00 +01:00

9.8 KiB

T06 runbook: the live pilot rotation under one admission (founder-run)

NK-WP-0056-T06. The executor rotates svc-authelia's LLDAP bind. Your only part in the run is the admission: you approve it at aal2 in informed-decision. Everything before that (sections 1 to 4) is one-time go-live of the standing paths. Section 6 is the run.

Each step prints a value-free result. Paste it back, and claude-code verifies it read-only and records it.

Commands:

  • Admin steps use K="kubectl --kubeconfig $HOME/.kube/config-railiance01".
  • OpenBao steps need bao login -method=oidc -path=netkingdom role=platform-admin (MFA) first.
  • Always run git pull --ff-only in a repo before using it.
# What Where Changes
1 Owner ingress (T16) approval-engine, audit-core, rapp two NetworkPolicies; read-only netcheck
2 Accept the case State Hub decision security-cases/nk-bind-executor-approval.yaml becomes accepted
3 Custody and audit sender railiance-platform, audit-core approve CCR-2026-0060, -0061, -0063; fence, policies, roles; audit seed; mint; ESO
4 KeyCape client net-kingdom bind the env var and register the client in one KeyCape restart
5 Admission net-kingdom (claude-code) admissions/BEA-YYYY-NNNN.yaml for the window you choose
6 The run net-kingdom bx ready, request, approve at aal2, run, status

1. Owner ingress (T16)

Apply each owner's policy object on its own. Live approval-engine also has informed-decision-review-ingress, which is not in its file. Applying the whole file would therefore be a broader change than this step.

K="kubectl --kubeconfig $HOME/.kube/config-railiance01"
cd ~/approval-engine && git pull --ff-only
python3 -c "import yaml,sys; yaml.safe_dump([d for d in yaml.safe_load_all(open('deploy/networkpolicies.yaml')) if d and d['metadata']['name']=='approval-engine-bounded-executor'][0], sys.stdout)" > /tmp/ae-bx.yaml
$K apply --server-side --dry-run=server -f /tmp/ae-bx.yaml && $K apply --server-side -f /tmp/ae-bx.yaml

audit-core: run steps 1 and 2 of audit-core docs/bounded-executor-receipts.md, "Founder runbook — nk-bounded-executor sender". Step 1 applies audit-core-nk-bounded-executor-ingress the same way. Step 2 is a read-only preflight, which also checks that the live scope ConfigMap carries nk-bounded-executor. This is railiance-platform's gate G2 before the audit seed.

Then prove the path from an executor-labelled pod. It holds no token and makes no login:

cd ~/rapp-bounded-executor && git pull --ff-only
make netcheck KUBECTL="$K"

Expected, on the last lines: "approval_engine_health": 200, "audit_core_health": 200 and "informed_decision_tcp": "ok", alongside the existing OpenBao, KeyCape and Authelia columns.

2. Accept the service-client case

Answer State Hub decision 59d570ef ("Accept SecurityCase nk-bind-executor-approval"). The case type requires owner and founder acceptance; you are both. claude-code then commits the case as accepted, with the acceptance record.

3. Custody and audit sender (railiance-platform, audit-core)

Follow railiance-platform docs/credential-lane-designs/nk-bind-executor-go-live.md, steps 0 to 7, in order:

  • Steps 0 to 3: session; approve CCR-2026-0060, -0061 and -0063 and confirm their bindings; the coding-agent fence; policies and roles.
  • Then tell claude-code. It commits the case as materialized: its OpenBao structure now exists. The broker mints only for a materialized, accepted case (gate G1). The case's hash changes with that commit, and step 5 derives REF from the committed file. So git pull --ff-only in net-kingdom before step 5.
  • Step 4: the audit seed, which is step 3 of audit-core's founder runbook. It needs gate G2 from section 1.
  • Step 5: mint the KeyCape client secret through the broker. No value is printed.
  • Step 6: ESO delivery to sso: KeyCape's keycape-nk-bind-executor-approval-client, and the executor's bounded-executor-tokens key audit-core.
  • Step 7: verify, which checks metadata, capabilities, logins and hashes only.

Section 4 needs step 7's Ready=True for keycape-nk-bind-executor-approval-client immediately before it runs.

4. KeyCape: bind and register in one restart

KeyCape runs one replica with strategy Recreate. Changing its env is itself a restart, and registering a confidential client whose secret is not bound makes the next start fail closed. So: pause the rollout, bind, register, then resume. That is one restart, with both changes (key-cape R4). Run it in a quiet minute: in-flight sign-ins are dropped.

cd ~/net-kingdom && git pull --ff-only
K="kubectl --kubeconfig $HOME/.kube/config-railiance01"
$K -n sso get externalsecret keycape-nk-bind-executor-approval-client -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'; echo   # True

C=$(mktemp); python3 sso-mfa/k8s/keycape/keycape-client.py client-from-case security-cases/nk-bind-executor-approval.yaml > "$C"
B=$HOME/.local/share/key-cape/verified-bin/e2b5fa1     # check-config binary from the deployed image (T09 runbook)
test -x "$B/keycape" || { mkdir -p "$B"; cid=$(docker create forgejo.coulomb.social/coulomb/key-cape@sha256:9e082dca78f65a4af3e92d3de3705b99e49ca9b298d9562ad0d37511c4306f8d); docker cp "$cid:/keycape" "$B/keycape"; docker rm "$cid"; }

# keycape-client.py uses the same route as $K (NK_KUBECONFIG, default ~/.kube/config-railiance01; NK-WP-0058-T07);
# it pins the cluster identity and refuses another cluster.
$K -n sso rollout pause deployment/keycape
$K -n sso patch deployment keycape --type=json -p '[
  {"op":"test","path":"/spec/template/spec/containers/0/name","value":"keycape"},
  {"op":"add","path":"/spec/template/spec/containers/0/env/-","value":{"name":"KEYCAPE_NK_BIND_EXECUTOR_APPROVAL_CLIENT_SECRET","valueFrom":{"secretKeyRef":{"name":"keycape-nk-bind-executor-approval-client","key":"client-secret"}}}}]'
python3 sso-mfa/k8s/keycape/keycape-client.py inspect --client-file "$C"
#   -> "deployment_env_bound": true, "change_needed": true; note before.uid and before.resource_version

Then put the noted values into the apply line, in place of UID and RV:

python3 sso-mfa/k8s/keycape/keycape-client.py apply --client-file "$C" \
  --case-acceptance "security-cases/nk-bind-executor-approval.yaml (accepted, hub decision 59d570ef)" \
  --check-config "$B/keycape" --expected-uid UID --expected-resource-version RV
$K -n sso rollout resume deployment/keycape
$K -n sso rollout status deployment/keycape --timeout=180s

If apply refuses, nothing in the registry changed. Undo the env patch before resuming: run $K -n sso rollout undo deployment/keycape, then $K -n sso rollout resume deployment/keycape, and paste the refusal.

claude-code then runs the type's verification. The executor's request Job (section 6) performs the token exchange per scope. The excess-scope refusal, the custody denials and the RBAC can-i denial are read-only checks.

5. Admission (claude-code prepares, you check)

claude-code runs bounded-executor/prepare_admission.py for the window you name (at most 2 h), with --commit set to the commit the pinned image was built from. It commits admissions/BEA-YYYY-NNNN.yaml. Read it: the plan, checks, revert steps, budget and window are what you will approve.

6. The run: four commands and the approval (NK-WP-0058-T04)

Run from ~/net-kingdom after git pull --ff-only here and in ~/rapp-bounded-executor:

BX="python3 tools/bounded-execution-run/bx.py"
$BX ready admissions/BEA-YYYY-NNNN.yaml   # one platform-admin login; expect "ready": true
$BX request                              # expect "outcome": "presented"
#   approve the memo in informed-decision: browser profile of admin, aal2, fresh OTP
$BX run                                  # one platform-admin login; preflight, open, run, close
$BX status                               # expect "outcome": "completed", "window_closed": true

Then tell claude-code. It checks the receipts in docs/evidence/<date>-<id>/ read-only, confirms the run in audit-core, runs posture feedback and records the evidence.

  • Each command prints one value-free line and is safe to repeat. run never starts a second run for an admission, and the state file keeps the approval id, so nothing is copied by hand.
  • ready and run each open one warden access openbao-platform-admin-login login (browser, MFA). Its child is silent and writes the receipts. In run, the window closes in the same child, also when the run fails.
  • Not ready? The line names each failed check. run refuses to open the window unless the preflight Job passes, so fix the cause and repeat.
  • Kill switch, at any moment: kubectl --kubeconfig ~/.kube/config-railiance01 -n sso delete job bounded-executor-<id>. The executor finishes the step in flight, then reverts. Before consume it spends nothing.
  • Emergency close: $BX close deletes the write role; it is safe to repeat.
Result Meaning
completed rotated, delivered, restarted, journey passed
reverted a check failed, and the previous value is back, checked
refused nothing was spent
not_started the preflight or the window did not pass; no window stayed open
stopped, unknown, revert_failed the alert in the result receipt names the step

Identity hygiene

  • Keep one browser profile per identity, and approve only in the admin profile. On 2026-11-04 the approval came from whichever identity was signed in (platform-root) and was refused.
  • Before approving, check the signed-in subject in informed-decision. Sign in again with OTP if the memo doesn't list: access-engine lists memos only within a 12-hour MFA window and opens them within 15 minutes.
  • The warden logins for ready and run are platform-admin in OpenBao, through KeyCape as admin.