From 8016f56f18c11534dfe7867784e342e8e959db02 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 27 Oct 2026 23:31:58 +0100 Subject: [PATCH] NK-WP-0055-T13: OpenBao break-glass ceremony with SSO down; RPF-WP-0064-T10 drill plan Adds the docs/openbao.md section: unseal, value-free held-token check, generate-root with in-memory OTP decode, minimal action, revoke-self with verification, post-event review record. Records that production's listener has unauthenticated generate-root disabled (OpenBao 2.5.4 default) and the owner decision this needs. Commands tested against a mock API. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 4515@bnt-lap001 Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354 --- WORK-RECORDS.md | 1 + docs/openbao.md | 229 ++++++++++++++++++ ...-0064-isolated-original-openbao-restore.md | 89 +++++++ 3 files changed, 319 insertions(+) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index cf2b810..7fe3b66 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -204,6 +204,7 @@ | task | RPF-WP-0064-T07 | done | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | | task | RPF-WP-0064-T08 | done | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | | task | RPF-WP-0064-T09 | todo | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | +| task | RPF-WP-0064-T10 | todo | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | | task | RPF-WP-0065-T01 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | | task | RPF-WP-0065-T02 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | | task | RPF-WP-0065-T03 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | diff --git a/docs/openbao.md b/docs/openbao.md index 054c71d..3df1d6b 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -193,6 +193,235 @@ escrow owner through an out-of-band channel. The initial root token is either revoked after a non-root platform-admin token exists or stored as offline break-glass material with the same handling as unseal shares. +## Break-glass: OpenBao with SSO down + +Use this when KeyCape or Authelia is down and an OpenBao action can't wait. It +belongs to NetKingdom NK-WP-0055-T13 and is drilled against an isolated restored +OpenBao only (RPF-WP-0064-T10). Never drill it on production. + +**What fails with SSO down.** Operator OIDC logins through `auth/netkingdom` +fail, and so does the warden lane. ops-warden SSH certificates fail too, +because the warden backend is OpenBao. The OpenBao UI tunnel +`openbao-ui-railiance01` is only usable if its SSH credential does not come +from warden. + +**What still works.** Cluster access uses the static-key tunnel +`k3s-api-railiance01` (as `tegwick`) plus the admin kubeconfig client +certificate. Both are founder-workstation material, with an age-sealed copy on +the Storage Share (net-kingdom crisis list items 4 and 9). Through that path +you can use `kubectl exec` into `openbao-0` and +`kubectl -n openbao port-forward pod/openbao-0 8200:8200`. + +**Custody facts.** The mechanism is manual Shamir unseal: 3 shares, threshold +2. The founder (platform-root) holds all three shares in the replicated browser +password safe, under the `kingpath.credentials` entry (crisis list item 3). The +founder also holds the initial root token offline. Its validity is unconfirmed. + +**Rules for every step.** + +- Enter values only at hidden prompts: `read -rs` or OpenBao's own + `(will be hidden)` prompt. +- Never put a share, token or OTP in argv, an exported environment variable, + a file, shell history, chat, State Hub or Git. +- Keep these values in unexported shell variables in one terminal and `unset` + them at the end. +- Clear the clipboard after each paste from the password safe. +- Run with `set +o history` (or a leading space with + `HISTCONTROL=ignorespace`) as defence in depth. +- Output is reduced with `jq` to fields that are not secret. Never print a + token, `accessor`, `encoded_token`, `otp` or share. + +### 1. Reach and unseal + +```bash +kubectl --context railiance01 -n openbao get pod openbao-0 +kubectl -n openbao exec openbao-0 -- bao status -format=json | jq '{initialized, sealed, t, n, progress}' +``` + +If `sealed` is `true`, enter two shares. Each one goes into OpenBao's hidden +prompt, which needs a TTY (`-ti`): + +```bash +kubectl -n openbao exec -ti openbao-0 -- bao operator unseal # share 1 at "Unseal Key (will be hidden):" +kubectl -n openbao exec -ti openbao-0 -- bao operator unseal # share 2 +kubectl -n openbao exec openbao-0 -- bao status -format=json | jq '{sealed, progress}' +``` + +Do not pass a share as an argument (`bao operator unseal `). That puts it +in argv on the workstation and in the pod. + +### 2. API session in one terminal + +```bash +set +o history +kubectl -n openbao port-forward pod/openbao-0 8200:8200 >/dev/null & PF_PID=$! +ADDR=http://127.0.0.1:8200 +bao_api() { # bao_api METHOD PATH [TOKEN_VAR [curl args...]]; JSON body on stdin (` and + `bao operator generate-root -generate-otp`: these put the OTP in argv. +- `bao operator generate-root -nonce=`: prompts for a share, hidden. +- `bao operator generate-root -decode= -otp=`: puts both values + in argv and prints the token. +- `bao operator generate-root -cancel` +- PGP mode: `-init -pgp-key=keybase:|`, then + `gpg --decrypt` of the base64-decoded `encoded_token`. + +Because `-decode` exposes the encoded token and OTP in argv and prints the +token, run the equivalent through the API with the OTP kept in memory: + +```bash +bao_api GET sys/generate-root/attempt ROOT -openbao-break-glass.json` (or the drill receipt for a +drill) and send NetKingdom a State Hub message. Fields: + +- **What:** + - each action and API path, with the reviewed body's sha256 + - whether the pod was unsealed + - the token check result from step 3 + - whether generate-root completed + - the HTTP status of `revoke-self` + - `revoked: true` +- **Why:** the triggering outage and why the OIDC path could not be used. +- **Who:** the operator and any witness, by role (`platform-root`, `tegwick`). +- **When:** UTC start and end, and the audit-log window. The audit device + records HMAC'd tokens only. +- **Follow-ups:** + - generate a new root token (no standing root) + - a decision on the held initial root token + - re-sealing of material on the Storage Share if anything moved + +The record never contains a share, token, accessor, OTP, nonce or encoded +token. + ## Auto-Unseal via Transit Seal (optional, NET-WP-0020 T4) `helm/openbao-values.yaml` carries a commented `seal "transit"` stanza inside diff --git a/workplans/RPF-WP-0064-isolated-original-openbao-restore.md b/workplans/RPF-WP-0064-isolated-original-openbao-restore.md index 5e2b12f..d84c99f 100644 --- a/workplans/RPF-WP-0064-isolated-original-openbao-restore.md +++ b/workplans/RPF-WP-0064-isolated-original-openbao-restore.md @@ -583,3 +583,92 @@ Named residuals: None of these blocks T15's OpenBao line. R1/R2 close in phase B. This is not owner acceptance of the full T15 packet or of the other three services. + +## T10 — Break-glass drill: generate-root ceremony and held root-token check (NK-WP-0055-T13) + +```task +id: RPF-WP-0064-T10 +status: todo +priority: high +depends_on: [RPF-WP-0064-T08] +``` + +Source: NetKingdom NK-WP-0055-T13. The ceremony is written in `docs/openbao.md`, section "Break-glass: +OpenBao with SSO down". The founder runs this drill against an **isolated restored OpenBao only, never +production**. KeyCape and Authelia are not used, which simulates SSO being down. + +**Drill mode (new adapter mode; the phase A path keeps its current behaviour).** + +The drill runs as a separate `--mode break-glass-drill` with its own signed admission +(`drill: "break-glass"`, attempt id `RPF-WP-0064-T10-`). It does the phase A restore and unseal, then: + +1. **Held root-token check.** A hidden prompt takes the founder's held initial root token. The drill calls + `auth/token/lookup-self` with it and records only `valid`, `policies`, `ttl == 0`, `expire_time == null` + and `orphan`. It never revokes or otherwise uses the held token. +2. **Fresh ceremony.** A `sys/generate-root/attempt` is started with a server-generated OTP. The founder + enters two shares **again** at hidden prompts, so the unseal shares are not reused silently as in phase A. + The token is decoded in memory and `lookup-self` must show `["root"]`. +3. **Minimal action.** One read-only action: `sys/auth` metadata. +4. **Revoke.** `revoke-self` must return 204, and a follow-up `lookup-self` must return 403. +5. **Cancelled attempt.** Start an attempt and cancel it (`DELETE sys/generate-root/attempt`), so the + cancel step is drilled too. + +**What the adapter must allow.** + +- **The isolated listener.** The adapter keeps `disable_unauthed_generate_root_endpoints = false` on the + `--network none` loopback listener only, as it does today. +- **`ALLOWED_WRITES`.** `sys/generate-root/attempt` needs both PUT and DELETE, and a second + `sys/generate-root/update` sequence must be allowed. +- **`ALLOWED_READS`.** `GET sys/generate-root/attempt` (status) must be added. `auth/token/lookup-self` + must be allowed with the held token and with the generated token. +- **Prompts.** One extra hidden prompt each for the held token and for the two re-entered shares. +- **Deadline.** Everything stays inside the 45 minute deadline, with the same teardown. +- **Unchanged rules.** No network, no KV `/data/` reads, no change to production. + +**Receipt fields (all free of secret values).** The drill writes a receipt v2 extension with: + +- `drill: "break-glass"` +- `held_root_token`: `valid`, `policies`, `ttl_zero` and `expire_time_null` +- `generate_root`: `started`, `shares_entered: 2`, `complete`, `otp_decode_ok`, `policies: ["root"]`, + `minimal_action_path` and `minimal_action_ok` +- `revocation`: `revoke_status: 204` and `post_revoke_lookup: 403` +- `cancel_drill`: `started_then_cancelled` +- `claims_not_made`, which additionally lists: + - `production_root_token_validity`: the result is valid only as of the snapshot. Name the snapshot (the + 2026-10-14 snapshot or a newer RPF-WP-0066 scheduled one) and its plaintext sha256. + - `production_generate_root_availability` + - `sso_down_access_path` + +The verifier gets matching adversarial fixtures that reject token, accessor, OTP, nonce, encoded-token and +share shapes. Offline tests and a native rehearsal use synthetic material and a fake held token. + +**Founder-run SSO-down walk (production, read-only).** Over `k3s-api-railiance01` with the admin kubeconfig, +the founder runs: + +- `kubectl -n openbao get pod openbao-0` +- `bao status` (a seal check) +- a `kubectl port-forward` reachability check + +There is no unseal and no generate-root on production. Record the result in the NK-WP-0055-T13 post-event +review record. + +**Owner decision for rapp-openbao.** The production listener has unauthenticated generate-root disabled +(OpenBao 2.5.4 default). This decision is a precondition for the ceremony to work on production without a +valid token. The choices are: + +- **(a)** an incident-time ConfigMap edit plus a restart +- **(b)** a standing loopback-only listener (`127.0.0.1:8210`) reachable only through `kubectl + port-forward` or `exec` + +**Follow-up (founder decision).** If the held initial root token is valid, revoke it on production after +the ceremony drill succeeds, or record why it is kept. + +**Done when:** + +- the drill mode is implemented and reviewed, with tests +- the native rehearsal passes +- a founder-signed admission exists for the window +- the founder has run the drill and the independent verifier passes on the receipt +- NetKingdom has a value-free post-event review record and the rapp-openbao listener decision + +No live run is part of the source preparation.