diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 8be5a3f..04d1c85 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -333,6 +333,7 @@ | task | NK-WP-0055-T13 | todo | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | task | NK-WP-0055-T14 | todo | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | task | NK-WP-0055-T15 | todo | — | workplans/NK-WP-0055-security-platform-production-readiness.md | +| task | NK-WP-0055-T16 | wait | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NET-IN-0001 | open | — | intakes/intakes.md | diff --git a/docs/crisis-custody-list.md b/docs/crisis-custody-list.md index 4b59ba6..2e530eb 100644 --- a/docs/crisis-custody-list.md +++ b/docs/crisis-custody-list.md @@ -32,9 +32,9 @@ circular dependency and is recorded as a gap, not as custody. | 1 | **`railiance-backup` age private key** (recipient `age1zvry…50syr3tsz`). Not the custodian key (item 1b) | Decrypts the identity backup bundle and the break-glass copy on Storage Share (`net-kingdom-kingpath-*.tar.age`) | Founder, password safe (stored as `railiance-backup.key`) | Yes, offline | Identity stages 2026-10-24; king-path verify drill 2026-10-27 (the helper refused the custodian key three times before the correct key was entered, as designed) | `TO CONFIRM`: number of copies, who else could recover if the founder cannot | | 1b | **Custodian age private key** (recipient `age1aq8t…qdepsh4`, `keys/age.pub` and `.sops.yaml`). Not the backup key | Decrypts the repository's SOPS custody files and greenfield bootstrap bundles (`docs/security-bootstrap-age-custody.md`) | Founder, password safe (stored as the custodian age private key) | Yes, offline | `TO CONFIRM` last proven use | Labels in the safe should name the recipient prefix so the two keys cannot be confused | | 2 | Storage Share `Backup` account app password | Downloads the protected ciphertext without OpenBao or SSO | Founder, independent custody `TO CONFIRM` | Yes, provider is outside the platform | Fresh retrieval 2026-10-24 | One provider copy only; residual risk accepted until 2027-01-31 (G1, NK-WP-0053) | -| 3 | OpenBao unseal shares (2 of 3 required in the restore drill) | Unseals a restored or production OpenBao; with the shares, `generate-root` mints a root token for break-glass (ceremony in `railiance-platform/docs/openbao.md`; production loopback listener 8210 live since 2026-10-28) | Founder as `platform-root` holds all three shares (single-holder custody: accepted exception, hub decision `37464824`, 2026-10-27), in the founder's replicated browser password safe under the `kingpath.credentials` pseudo-URL entry (confirmed 2026-10-27) | Yes: the safe is independent of NetKingdom SSO and OpenBao; it depends on the browser vendor's sync account and its own factor | Original shares used in the isolated restore RPF-WP-0064-T07, 2026-10-24 | Single holder = single point of loss (reviewed at NK-WP-0055-T09). The founder holds the initial root token offline; the king-path drill (2026-10-28) found it **valid** as of the 2026-10-14 snapshot (root policy, no TTL, orphan). Revoke on production or keep escrowed: founder decision pending | +| 3 | OpenBao unseal shares (2 of 3 required in the restore drill) | Unseals a restored or production OpenBao; with the shares, `generate-root` mints a root token for break-glass (ceremony in `railiance-platform/docs/openbao.md`; production loopback listener 8210 live since 2026-10-28) | Founder as `platform-root` holds all three shares (single-holder custody: accepted exception, hub decision `37464824`, 2026-10-27), in the founder's replicated browser password safe under the `kingpath.credentials` pseudo-URL entry (confirmed 2026-10-27) | Yes: the safe is independent of NetKingdom SSO and OpenBao; it depends on the browser vendor's sync account and its own factor | Original shares used in the isolated restore RPF-WP-0064-T07, 2026-10-24 | Single holder = single point of loss (reviewed at NK-WP-0055-T09). The founder holds the initial root token offline; the king-path drill (2026-10-28) found it **valid** as of the 2026-10-14 snapshot (root policy, no TTL, orphan). Founder decision `7ce684ec` (2026-10-28): stays escrowed during builder mode, revoked at production entry (NK-WP-0055-T16) | | 4 | `platform-root` / king account and factors | Final authority; break-glass | Independently held password safe or offline packet (Standard §4; custody doc) | **No if used through KeyCape**: its login and OTP are validated by the services being recovered | OTP validated against restored privacyIDEA 2026-10-24 (isolated) | `TO CONFIRM` the offline path that works with SSO down (OpenBao shares, item 3) and record it | -| 5 | Founder admission signing key (SSH ed25519, principal `bernd`, fingerprint `SHA256:NekDABfw8K8IBYznjREg7jPpcK/BzCgO1pH1ebT/FXM`) | Signs restore and drill admissions for the OpenBao isolated restore (namespace `railiance-openbao-restore-attempt`) and secrets-engine admissions | Private key: founder workstation `~/.ssh/gexec_approver_ed25519` (passphrase-protected; signed the T40, `-02` and `-03` admissions); founder decision 2026-10-28: also keep it in the password safe under the kingpath entry, **not** in the Storage Share bundle, so the backup age key alone cannot admit a restore. Public anchor (non-secret, needed on any recovery workstation): `/etc/railiance-openbao-restore/allowed_signers` and `/etc/secrets-engine/admission/allowed_signers`, one line each: `bernd namespaces="railiance-openbao-restore-attempt" ssh-ed25519 ssh-ed25519` ; the secrets-engine file uses namespace `secrets-engine-recovery-attempt` | Yes, offline | Signed the T40 admission 2026-10-23 | Store the private key in the safe; re-create both anchor files from this row on a recovery workstation. The file name says `gexec_approver`, but the key's role is admission signing | +| 5 | Founder admission signing key (SSH ed25519, principal `bernd`, fingerprint `SHA256:NekDABfw8K8IBYznjREg7jPpcK/BzCgO1pH1ebT/FXM`) | Signs restore and drill admissions for the OpenBao isolated restore (namespace `railiance-openbao-restore-attempt`) and secrets-engine admissions | Private key: founder workstation `~/.ssh/gexec_approver_ed25519` (passphrase-protected; signed the T40, `-02` and `-03` admissions); founder decision 2026-10-28: also keep it in the password safe under the kingpath entry, **not** in the Storage Share bundle, so the backup age key alone cannot admit a restore. Public anchor (non-secret, needed on any recovery workstation): `/etc/railiance-openbao-restore/allowed_signers` and `/etc/secrets-engine/admission/allowed_signers`, one line each: `bernd namespaces="railiance-openbao-restore-attempt" ssh-ed25519 ssh-ed25519` ; the secrets-engine file uses namespace `secrets-engine-recovery-attempt` | Yes, offline | Signed the T40 admission 2026-10-23 | Private key stored in the password safe kingpath entry (founder, 2026-10-28). Re-create both anchor files from this row on a recovery workstation. The file name says `gexec_approver`, but the key's role is admission signing | | 6 | `admin` password and OTP token | Day-to-day platform administration and, per intent, recovery operation | Founder; OTP enrolled 2026-10-24 | **No**: login and token checks need KeyCape, LLDAP and privacyIDEA | Token not yet in any restored backup; first restore proof after the 2026-10-25 03:45 UTC capture | Same account as the directory bind used by Authelia and privacyIDEA until the split in NK-WP-0054-T05 | | 7 | `pi-admin` credentials (privacyIDEA internal admin) | Token repair and re-enrolment | Founder's replicated browser password safe (confirmed 2026-10-27) | Custody yes (replicated, independent of SSO); use needs a running privacyIDEA | Not yet drilled | Add a pi-admin check to the drill (NK-WP-0053-T04) | | 8 | ~~Ops bundle (`make creds-bundle`)~~ **Retired 2026-10-27** (founder decision) | — | — | — | Never drilled; none existed on the workstation | Superseded by the daily identity backup, which carries the current service secrets. `make creds-bundle` packs only greenfield `creds-generate` output and stays a greenfield bootstrap step | diff --git a/docs/evidence/2026-10-28-king-path-drill.md b/docs/evidence/2026-10-28-king-path-drill.md index e1da85b..a94c982 100644 --- a/docs/evidence/2026-10-28-king-path-drill.md +++ b/docs/evidence/2026-10-28-king-path-drill.md @@ -40,10 +40,10 @@ keys cannot be confused again. 1. **A standing, non-expiring root token very likely exists on production.** It was valid at the 2026-10-14 snapshot and has not been revoked since. The architecture asks that the initial root token be revoked, or escrowed as break-glass material. The `generate-root` path now works on production and - is drilled, so it can replace that token. Founder decision needed: revoke it on production, or keep it - escrowed and record why. + is drilled, so it can replace that token. Founder decision `7ce684ec`: keep it escrowed during builder mode and + revoke it at production entry (NK-WP-0055-T16, guarded script in railiance-platform `95b5a19`). 2. **Production read-only SSO-down walk:** still to run (RPF-WP-0064-T10 runbook). -3. **Admission signing key:** it is `~/.ssh/gexec_approver_ed25519`. It needs a copy in the password safe - under the kingpath entry (crisis list item 5). +3. **Admission signing key:** it is `~/.ssh/gexec_approver_ed25519`. The founder stored a copy in the password + safe under the kingpath entry on 2026-10-28 (crisis list item 5). 4. **Not drilled:** using the restored kubeconfig and SSH key against the cluster. Provider console access is not drilled either. diff --git a/workplans/NK-WP-0055-security-platform-production-readiness.md b/workplans/NK-WP-0055-security-platform-production-readiness.md index 579dd52..b5a3430 100644 --- a/workplans/NK-WP-0055-security-platform-production-readiness.md +++ b/workplans/NK-WP-0055-security-platform-production-readiness.md @@ -52,6 +52,11 @@ For **Bootstrap and recovery**, this plan adds three requirements, drawn from NK - **Recurring drill:** restores are re-proved on a schedule, not just once. - **Credential cleanup:** the remaining bootstrap-era credentials are rotated or revoked. +At production entry, these one-time actions must also have run: + +- **Revoke the initial root token.** The held initial OpenBao root token is revoked with + `railiance-platform/scripts/openbao_revoke_initial_root_token.py`, and its receipt shows `revoked` (T16). + Readiness does not mean HA. HA is a separate decision, and the current estate runs on one node. @@ -357,6 +362,26 @@ state_hub_task_id: "d9c0584e-10a1-5edf-a1f9-cec5738f94a8" **Done when:** every service, database, OpenBao-issued, client-secret and break-glass credential lane is listed with its blast radius and verification step, linked to its owner lane (RPF-WP-0035, RPF-WP-0055-T01, T06). +## Revoke the held initial OpenBao root token at production entry + +```task +id: NK-WP-0055-T16 +status: wait +priority: high +assignee: claude-code +blocking_reason: "Founder decision 7ce684ec: the token stays escrowed during builder mode and is revoked at production entry." +``` + +The king-path drill (2026-10-28) found the held initial root token valid as of the 2026-10-14 snapshot: `root`, +orphan, no TTL. The guarded script `railiance-platform/scripts/openbao_revoke_initial_root_token.py` (`95b5a19`, +7 tests) reads the token at a hidden prompt. It refuses to revoke unless the loopback `generate-root` listener +answers, so a replacement break-glass path exists first, and it writes a value-free receipt. The runbook is in +`railiance-platform/docs/openbao.md`. + +**Done when:** at production entry, the founder runs `revoke` under the production-entry decision id; the +receipt shows `revoked` (revoke-self 204, then lookup-self 403); the token is removed from the password safe; +and crisis list item 3 is updated. + ## Close readiness and declare production ```task @@ -364,7 +389,7 @@ id: NK-WP-0055-T09 status: wait priority: high assignee: claude-code -depends_on: [NK-WP-0055-T01, NK-WP-0055-T03, NK-WP-0055-T04, NK-WP-0055-T05, NK-WP-0055-T06, NK-WP-0055-T07, NK-WP-0055-T08, NK-WP-0055-T10, NK-WP-0055-T11, NK-WP-0055-T12, NK-WP-0055-T13, NK-WP-0055-T14, NK-WP-0055-T15] +depends_on: [NK-WP-0055-T01, NK-WP-0055-T03, NK-WP-0055-T04, NK-WP-0055-T05, NK-WP-0055-T06, NK-WP-0055-T07, NK-WP-0055-T08, NK-WP-0055-T10, NK-WP-0055-T11, NK-WP-0055-T12, NK-WP-0055-T13, NK-WP-0055-T14, NK-WP-0055-T15, NK-WP-0055-T16] blocking_reason: "Every readiness area must be met or an accepted exception." state_hub_task_id: "0630b0ea-dec1-5355-8b08-0d317d4a3c9b" ```