Port the seven console runbooks to native runbook-pack/v0.2 packs (NK-WP-0044-T07)
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

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

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 6969@bnt-lap001
Assistant-Session: e8ae73f2-8385-4c9c-b714-94c3c52cdbc0
This commit is contained in:
tegwick 2026-09-29 23:14:52 +02:00
parent ecd7c619c5
commit 1fbebb2d94
8 changed files with 594 additions and 3 deletions

View file

@ -0,0 +1,79 @@
spec: runbook-pack/v0.2
id: nk.openbao-emergency-lockdown
title: OpenBao emergency lock-down
owner: net-kingdom
outcome: OpenBao is sealed, sealed state is confirmed, and the drill or incident outcome is recorded without secrets.
engine: native
confirm_phrase: I understand this cannot be undone
framing:
- Sealing stops all access to stored assets until a quorum of shares unseals it again.
- Confirm the quorum is available before sealing outside an incident.
- Ported from the security bootstrap console runbook "Emergency lock-down".
actors:
- id: operator
role: OpenBao ceremony operator
description: Holds a root or sudo-capable token and seals the instance.
privileges: [openbao-root-or-sudo]
datapoints:
- id: evidence_path
label: Emergency drill evidence file
type: path
default: /tmp/netkingdom-openbao-emergency-drill/evidence.json
dimensions: {actor: operator, kind: location, subsystem: openbao}
ask_in: seal
stages:
- id: seal
title: Seal and confirm
goal: OpenBao reports Sealed true.
steps:
- id: seal-now
title: Seal OpenBao
actor: operator
owner: openbao
command: |
printf "OpenBao token: " >&2; read -rs OPENBAO_TOKEN; printf "\n" >&2
printf "%s\n" "$OPENBAO_TOKEN" | kubectl exec -i -n openbao openbao-0 -- sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao operator seal'
unset OPENBAO_TOKEN
reversible: false
idempotent: idempotent
idempotent_note: Sealing an already sealed instance changes nothing.
auto_confirm: manual-only
rollback: Unseal with a quorum of shares (attended ceremony).
verify:
done_when: The seal command succeeded
expect: manual
- id: confirm-sealed
title: Confirm sealed status
actor: operator
owner: openbao
requires: [seal-now]
command: kubectl exec -n openbao openbao-0 -- bao status
idempotent: idempotent
verify:
done_when: Sealed is true
expect: manual
gates:
- id: sealed
title: OpenBao is sealed
goal: contained-exposure
blocking: true
downstream_risk: Stored assets stay reachable during the incident.
checks:
- done_when: Status reports Sealed true
expect: manual
- id: record
title: Record the outcome
goal: Non-secret drill or incident evidence validates.
requires: [seal]
steps:
- id: validate-evidence
title: Validate the emergency evidence
actor: operator
owner: railiance-platform
uses: [evidence_path]
command: make -C ../railiance-platform openbao-validate-emergency-evidence OPENBAO_EMERGENCY_EVIDENCE={{evidence_path}}
idempotent: idempotent
verify:
done_when: The validator accepts the evidence
command: make -C ../railiance-platform openbao-validate-emergency-evidence OPENBAO_EMERGENCY_EVIDENCE={{evidence_path}}
expect: exit-0

View file

@ -0,0 +1,84 @@
spec: runbook-pack/v0.2
id: nk.openbao-key-compromise
title: OpenBao key material compromised
owner: net-kingdom
outcome: The exposure is recorded, reset versus rotation is chosen and carried out, and only the non-secret outcome is on record.
engine: native
confirm_phrase: I understand this cannot be undone
framing:
- "Nothing secret is recorded here: not init output, shares, tokens or their fragments."
- Treat everything the exposed material could reach as untrusted until the response is complete.
- Ported from the security bootstrap console runbook "Key material compromised".
actors:
- id: operator
role: OpenBao ceremony operator
description: Runs the attended OpenBao commands and records the outcome.
privileges: [openbao-root-or-sudo]
datapoints:
- id: response
label: Chosen response
type: enum
values: [reset, rotate-keys]
default: rotate-keys
help: Reset when the instance holds nothing yet worth keeping; rotate when it holds live data.
dimensions: {actor: operator, kind: decision, subsystem: openbao}
ask_in: respond
- id: exposure_summary
label: Non-secret exposure summary
type: text
default: ""
help: What escaped, where, when. No secret values.
dimensions: {actor: operator, kind: evidence, subsystem: openbao}
ask_in: assess
stages:
- id: assess
title: Assess the exposure
goal: You know what escaped and which paths it could reach.
steps:
- id: status
title: Check OpenBao state
actor: operator
owner: openbao
command: kubectl exec -n openbao openbao-0 -- bao status
idempotent: idempotent
verify:
done_when: Status is shown
expect: manual
- id: inspect-paths
title: Inspect affected mounts and auth methods
actor: operator
owner: openbao
requires: [status]
command: |
printf "OpenBao token: " >&2; read -rs OPENBAO_TOKEN; printf "\n" >&2
printf "%s\n" "$OPENBAO_TOKEN" | kubectl exec -i -n openbao openbao-0 -- sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao secrets list; bao auth list'
unset OPENBAO_TOKEN
idempotent: idempotent
verify:
done_when: The affected paths are listed and noted
expect: manual
- id: respond
title: Reset or rotate
goal: The exposed material is no longer valid.
requires: [assess]
steps:
- id: carry-out
title: Carry out the chosen response
actor: operator
owner: openbao
uses: [response]
description: "If rotate-keys, run the pack nk.openbao-rotate-unseal-keys and revoke exposed tokens with nk.openbao-token-revocation. If reset, reinitialize per the attended ceremony runbook (docs/openbao-attended-ceremony-runbook.md); this destroys stored data."
reversible: false
idempotent: non-idempotent
verify:
done_when: The response is complete and the old material no longer works
expect: manual
gates:
- id: response-complete
title: The compromise response is complete
goal: contained-exposure
blocking: true
downstream_risk: Production trust would rest on material an outsider may hold.
checks:
- done_when: Exposed shares and tokens are invalid and the non-secret outcome is recorded
expect: manual

View file

@ -0,0 +1,125 @@
spec: runbook-pack/v0.2
id: nk.openbao-restore-drill
title: OpenBao restore drill
owner: net-kingdom
outcome: A Raft snapshot was taken, encrypted, restored into an isolated instance and verified, and the non-secret evidence validates.
engine: native
confirm_phrase: I understand this cannot be undone
framing:
- Never restore into namespace openbao; use a disposable cluster, VM or namespace.
- The plaintext snapshot is removed once encrypted; only hashes and flags go into the evidence.
- Execute after initial configuration exists and before live secrets move in.
- Ported from the security bootstrap console runbook "Restore drill".
actors:
- id: operator
role: OpenBao ceremony operator
description: Takes the snapshot and runs the isolated restore.
privileges: [openbao-root-or-sudo]
datapoints:
- id: drill_dir
label: Drill workspace
type: path
default: /tmp/netkingdom-openbao-restore-drill
dimensions: {actor: operator, kind: location, subsystem: openbao}
ask_in: snapshot
- id: age_recipient
label: Custodian age recipient (public key)
type: string
default: ""
help: The age public key; never a private key.
dimensions: {actor: operator, kind: reference, subsystem: sops-age}
ask_in: snapshot
stages:
- id: snapshot
title: Snapshot and encrypt
goal: An encrypted snapshot with recorded hashes exists and no plaintext remains.
steps:
- id: workspace
title: Prepare the workspace
actor: operator
owner: workstation
uses: [drill_dir]
command: mkdir -p {{drill_dir}} && chmod 700 {{drill_dir}}
idempotent: idempotent
verify:
done_when: The directory exists
command: test -d {{drill_dir}}
expect: exit-0
- id: take
title: Take and copy out the snapshot
actor: operator
owner: openbao
requires: [workspace]
uses: [drill_dir]
command: |
printf "OpenBao token: " >&2; read -rs OPENBAO_TOKEN; printf "\n" >&2
printf "%s\n" "$OPENBAO_TOKEN" | kubectl exec -i -n openbao openbao-0 -- sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao operator raft snapshot save /tmp/openbao-raft.snap'
unset OPENBAO_TOKEN
kubectl cp openbao/openbao-0:/tmp/openbao-raft.snap {{drill_dir}}/openbao-raft.snap
kubectl exec -n openbao openbao-0 -- rm -f /tmp/openbao-raft.snap
sha256sum {{drill_dir}}/openbao-raft.snap | tee {{drill_dir}}/openbao-raft.snap.sha256
idempotent: idempotent
idempotent_note: Each run overwrites the local copy with a fresh snapshot.
auto_confirm: manual-only
rollback: Delete the local snapshot and its hash file.
verify:
done_when: The snapshot exists locally
command: test -s {{drill_dir}}/openbao-raft.snap
expect: exit-0
- id: encrypt
title: Encrypt and remove the plaintext
actor: operator
owner: workstation
requires: [take]
uses: [drill_dir, age_recipient]
command: |
age -r {{age_recipient}} -o {{drill_dir}}/openbao-raft.snap.age {{drill_dir}}/openbao-raft.snap
sha256sum {{drill_dir}}/openbao-raft.snap.age | tee {{drill_dir}}/openbao-raft.snap.age.sha256
shred -u {{drill_dir}}/openbao-raft.snap
reversible: false
idempotent: non-idempotent
verify:
done_when: The encrypted file exists and the plaintext is gone
command: test -s {{drill_dir}}/openbao-raft.snap.age && test ! -e {{drill_dir}}/openbao-raft.snap
expect: exit-0
- id: restore
title: Restore in isolation
goal: The snapshot restores into a fresh isolated instance and verifies.
requires: [snapshot]
steps:
- id: isolated-restore
title: Run the isolated restore proof
actor: operator
owner: openbao
description: "1. Use a disposable cluster, VM or namespace, never namespace=openbao. 2. Deploy a fresh OpenBao with empty storage. 3. Decrypt the snapshot only inside the isolated workspace. 4. bao operator raft snapshot restore -force /tmp/openbao-raft.snap. 5. Unseal with the current drill shares. 6. Verify status, mounts, auth methods, policies and a non-production test secret read. 7. Destroy the isolated environment."
idempotent: non-idempotent
auto_confirm: manual-only
rollback: Destroy the isolated environment.
verify:
done_when: Status, mounts, auth, policies and a test secret read verified, and the environment destroyed
expect: manual
gates:
- id: evidence-valid
title: Restore evidence validates
goal: recoverable-custody
blocking: true
downstream_risk: A backup that was never restored is not a backup.
checks:
- done_when: The non-secret evidence JSON validates
command: make -C ../railiance-platform openbao-validate-restore-evidence OPENBAO_RESTORE_EVIDENCE={{drill_dir}}/evidence.json
expect: exit-0
- id: after
title: Post-restore readiness
goal: Post-unseal checks pass.
requires: ["gate:evidence-valid"]
steps:
- id: post-unseal
title: Run the post-restore readiness check
actor: operator
owner: railiance-platform
command: make -C ../railiance-platform openbao-verify-post-unseal
idempotent: idempotent
verify:
done_when: The readiness check passes
command: make -C ../railiance-platform openbao-verify-post-unseal
expect: exit-0

View file

@ -0,0 +1,101 @@
spec: runbook-pack/v0.2
id: nk.openbao-rotate-unseal-keys
title: Generate new OpenBao unseal keys
owner: net-kingdom
outcome: OpenBao has replacement unseal shares held by their custodians and the rotation is confirmed.
engine: native
confirm_phrase: I understand this cannot be undone
framing:
- Shares and tokens are typed at hidden prompts only; never on a command line or in a log.
- Route each new share directly to its custody holder.
- Needs an unsealed OpenBao and a quorum of current shares.
- Ported from the security bootstrap console runbook "Generate new unseal keys".
actors:
- id: operator
role: OpenBao ceremony operator
description: Runs the rotation with the current-share holders present.
privileges: [openbao-root-or-sudo]
datapoints:
- id: keys_rotated
label: New unseal keys generated
type: enum
values: ["yes", "no"]
default: "no"
dimensions: {actor: operator, kind: state, subsystem: openbao}
ask_in: rotate
stages:
- id: rotate
title: Rotate the shares
goal: New 3-share, threshold-2 shares exist and are with their holders.
steps:
- id: status-before
title: Check for a rotation in progress
actor: operator
owner: openbao
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'printf "OpenBao token: " >&2; stty -echo; read -r BAO_TOKEN; stty echo; printf "\n" >&2; export BAO_TOKEN; bao operator rotate-keys -status'
idempotent: idempotent
auto_confirm: manual-only
verify:
done_when: You know whether a rotation is already in progress
expect: manual
- id: init
title: Start the rotation (once)
actor: operator
owner: openbao
requires: [status-before]
description: Run once. If a rotation is already in progress, do not rerun init; submit existing shares instead.
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'printf "OpenBao token: " >&2; stty -echo; read -r BAO_TOKEN; stty echo; printf "\n" >&2; export BAO_TOKEN; bao operator rotate-keys -init -key-shares=3 -key-threshold=2'
idempotent: non-idempotent
auto_confirm: manual-only
rollback: Cancel the rotation (step "cancel"); no share has changed until the threshold is reached.
verify:
done_when: A nonce was issued
expect: manual
- id: submit
title: Submit current shares
actor: operator
owner: openbao
requires: [init]
description: Enter the nonce, the token and one current share per prompt. Repeat with distinct current shares until progress reaches the threshold.
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'printf "Rotation nonce: " >&2; read -r ROTATION_NONCE; printf "OpenBao token: " >&2; stty -echo; read -r BAO_TOKEN; stty echo; printf "\n" >&2; export BAO_TOKEN; bao operator rotate-keys -nonce="$ROTATION_NONCE"'
reversible: false
idempotent: non-idempotent
auto_confirm: manual-only
verify:
done_when: The threshold is reached and new shares are shown once on screen
expect: manual
- id: distribute
title: Route the new shares to their holders
actor: operator
owner: custody
requires: [submit]
description: Give each new share to its custody holder directly; record only that each holder confirmed.
idempotent: non-idempotent
verify:
done_when: Every holder confirmed receipt
expect: manual
- id: cancel
title: Cancel the rotation (only if something went wrong)
actor: operator
owner: openbao
requires: [init]
description: Use if the nonce, share handling or ceremony context is wrong. Needs a root or sudo token.
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'printf "OpenBao token: " >&2; stty -echo; read -r BAO_TOKEN; stty echo; printf "\n" >&2; export BAO_TOKEN; bao operator rotate-keys -cancel'
idempotent: idempotent
auto_confirm: manual-only
verify:
done_when: The rotation is cancelled
expect: manual
gates:
- id: keys-rotated
title: The new shares are in custody
goal: recoverable-custody
blocking: true
downstream_risk: Old shares stay valid or new ones are lost; either breaks recovery.
checks:
- done_when: Every holder confirmed receipt and the old shares are destroyed
expect: manual

View file

@ -0,0 +1,65 @@
spec: runbook-pack/v0.2
id: nk.openbao-token-revocation
title: OpenBao token revocation
owner: net-kingdom
outcome: Short-lived, temporary or disclosed OpenBao tokens are revoked without token values on the local command line.
engine: native
confirm_phrase: I understand this cannot be undone
framing:
- Token values are typed at hidden prompts only; the accessor is the handle for a disclosed token.
- Needs an initialized OpenBao instance.
- Ported from the security bootstrap console runbook "OpenBao token revocation".
actors:
- id: operator
role: OpenBao ceremony operator
description: Revokes tokens with a root or sudo-capable token.
privileges: [openbao-root-or-sudo]
datapoints:
- id: which
label: What to revoke
type: enum
values: [current-helper-token, disclosed-by-accessor]
default: current-helper-token
dimensions: {actor: operator, kind: decision, subsystem: openbao}
ask_in: revoke
stages:
- id: revoke
title: Revoke
goal: The token no longer works.
steps:
- id: revoke-self
title: Revoke the current helper token
actor: operator
owner: openbao
description: For the token in the pod token helper, usually right after an attended check.
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'export BAO_ADDR=http://127.0.0.1:8200; bao token lookup >/dev/null; bao token revoke -self'
reversible: false
idempotent: non-idempotent
auto_confirm: manual-only
verify:
done_when: The revoke command succeeded
expect: manual
- id: revoke-accessor
title: Revoke a disclosed token by accessor
actor: operator
owner: openbao
description: Prompts for a root or sudo token and the accessor of the disclosed token.
command: |
kubectl exec -it -n openbao openbao-0 -- sh -lc 'printf "OpenBao token: " >&2; stty -echo; read -r BAO_TOKEN; stty echo; printf "\n" >&2; export BAO_TOKEN; printf "Token accessor to revoke: " >&2; read -r TARGET_ACCESSOR; bao token revoke -accessor "$TARGET_ACCESSOR"; unset TARGET_ACCESSOR'
reversible: false
idempotent: idempotent
idempotent_note: Revoking an already revoked accessor fails harmlessly.
auto_confirm: manual-only
verify:
done_when: The accessor is revoked
expect: manual
gates:
- id: revoked
title: The token is dead
goal: contained-exposure
blocking: true
downstream_risk: A disclosed token keeps its access.
checks:
- done_when: Using the revoked token is refused
expect: manual

View file

@ -0,0 +1,59 @@
spec: runbook-pack/v0.2
id: nk.privacyidea-realm-repair
title: privacyIDEA realm repair
owner: net-kingdom
outcome: The coulomb realm, LLDAP resolver and self-service policies exist again, and platform-root TOTP is enrolled in the repaired realm.
engine: native
framing:
- Passwords are entered at the script's prompts only; never on a command line, in chat or in a file.
- Ported from the security bootstrap console runbook "privacyIDEA realm repair".
actors:
- id: identity-admin
role: Identity admin
description: Holds the pi-admin and LLDAP bind passwords and runs the attended repair.
privileges: [privacyidea-admin, lldap-bind]
datapoints:
- id: realm_repaired
label: Realm repaired and verified
type: enum
values: ["yes", "no"]
default: "no"
dimensions: {actor: identity-admin, kind: state, subsystem: privacyIDEA}
ask_in: repair
stages:
- id: repair
title: Repair the realm
goal: The realm, resolver and policies are recreated and the T06 verification passes.
steps:
- id: run-repair
title: Run the attended realm repair
actor: identity-admin
owner: privacyIDEA
description: Prompts for the pi-admin and LLDAP bind passwords, recreates the realm, resolver, self-enrollment and passthrough policies, then runs T06 verification. Passwords stay in a private temp directory removed on exit.
command: bash sso-mfa/k8s/privacyidea/repair-realm-live.sh
idempotent: idempotent
idempotent_note: Recreates the same objects; safe to rerun.
auto_confirm: manual-only
verify:
done_when: The script's T06 verification reports success
expect: manual
- id: reenroll-totp
title: Enroll or re-enroll platform-root TOTP
actor: identity-admin
owner: privacyIDEA
requires: [run-repair]
description: In privacyIDEA self-service, log in as platform-root and enroll TOTP in the repaired realm. Record the recovery codes in the platform-root safe entry, not here.
idempotent: non-idempotent
rollback: Delete the token in self-service and enroll again.
verify:
done_when: platform-root can log in with a TOTP code
expect: manual
gates:
- id: realm-usable
title: The repaired realm is usable
goal: recoverable-identity
blocking: true
downstream_risk: Logins that need MFA fail for every user of the realm.
checks:
- done_when: The T06 verification passed and platform-root TOTP works
expect: manual

View file

@ -0,0 +1,78 @@
spec: runbook-pack/v0.2
id: nk.user-lifecycle-dry-run
title: Non-root user lifecycle dry-run
owner: net-kingdom
outcome: A non-root user is created, verified, locked and offboarded, LLDAP, groups, MFA and KeyCape claims are checked, and non-secret evidence validates.
engine: native
framing:
- Uses a throwaway subject; no real account is touched.
- Evidence is non-secret; passwords and codes never appear in it.
- Ported from the security bootstrap console runbook "User lifecycle dry-run (T06)"; see NET-WP-0019.
actors:
- id: custodian
role: Platform custodian
description: Runs the dry-run and reviews the evidence.
privileges: [lldap-admin, keycape-read]
datapoints:
- id: subject
label: Dry-run subject
type: string
default: dryrun-user
pattern: "[a-z0-9-]+"
dimensions: {actor: custodian, kind: identity, subsystem: LLDAP}
ask_in: run
- id: email
label: Dry-run email
type: string
default: dryrun-user@example.com
dimensions: {actor: custodian, kind: identity, subsystem: LLDAP}
ask_in: run
- id: display
label: Display name
type: string
default: Dry Run
dimensions: {actor: custodian, kind: identity, subsystem: LLDAP}
ask_in: run
- id: evidence_path
label: Evidence file
type: path
default: /tmp/netkingdom-onboarding-dry-run/evidence.json
dimensions: {actor: custodian, kind: location, subsystem: LLDAP}
ask_in: run
stages:
- id: run
title: Run the lifecycle
goal: Create, verify, lock and offboard completed for the subject.
steps:
- id: template
title: Print the evidence template
actor: custodian
owner: net-kingdom
command: make security-bootstrap-onboarding-dry-run-template
idempotent: idempotent
verify:
done_when: A non-secret evidence JSON template is printed
expect: manual
- id: dry-run
title: Run the dry-run orchestrator
actor: custodian
owner: LLDAP
requires: [template]
uses: [subject, email, display]
command: make security-bootstrap-onboarding-dry-run SUBJECT={{subject}} EMAIL={{email}} DISPLAY="{{display}}"
idempotent: non-idempotent
auto_confirm: manual-only
rollback: Offboard the subject (the orchestrator's final step); delete it in LLDAP if that step did not run.
verify:
done_when: The orchestrator finished create, verify, lock and offboard
expect: manual
gates:
- id: evidence-valid
title: The dry-run evidence validates
goal: governed-lifecycle
blocking: true
downstream_risk: Onboarding is trusted without proof that offboarding works.
checks:
- done_when: The evidence validator accepts the file
command: make security-bootstrap-validate-onboarding-dry-run ONBOARDING_DRY_RUN_EVIDENCE={{evidence_path}}
expect: exit-0

View file

@ -109,9 +109,7 @@ replaced by `runbooks-validate`, and `docs/tutorials/` becomes a pointer.
```task
id: NK-WP-0044-T07
status: wait
needs_human: true
blocking_reason: "Content work, not scheduled: needs a decision to start. The seven runbooks map without gaps (runbook-tutorials/docs/console-mapping.md)."
status: done
priority: low
state_hub_task_id: "8628d5eb-6e5d-501d-a8f5-960d6b5db8f4"
```
@ -120,6 +118,8 @@ The security bootstrap console's seven runbooks (privacyIDEA realm repair, key m
restore drill, token revocation, user lifecycle dry-run) can become native v0.2 packs. They are linear procedures with 23 command blocks and need
none of the gaps G1 to G7 in `runbook-tutorials/docs/console-mapping.md`. The console itself stays a permanent legacy pack.
**Ported on 2026-09-29**: `runbooks/privacyidea-realm-repair`, `openbao-key-compromise`, `openbao-rotate-unseal-keys`, `openbao-emergency-lockdown`, `openbao-restore-drill`, `openbao-token-revocation` and `user-lifecycle-dry-run`. All validate (`make runbooks-validate`, 11 packs). They are `unexercised`: no command was run. Secret-handling steps are `auto_confirm: manual-only`, irreversible steps use the pack `confirm_phrase`, and token and share entry stays at hidden prompts.
## Acceptance Criteria
- The console still works and is launched by the engine without changes to it.