From e2540529f0de4a7334794fb11b3f645cdc0e5d2c Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 25 May 2026 18:31:48 +0200 Subject: [PATCH] Add OpenBao emergency lockdown runbook --- .../security_bootstrap_console.py | 162 ++++++++++++++++-- ...-custody-and-openbao-identity-bootstrap.md | 6 + 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index e1f6129..181eeed 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -594,6 +594,7 @@ def merged_approval_metadata( "openbao_trial_material_exposed", "openbao_compromise_response_complete", "openbao_unseal_keys_rotated", + "openbao_emergency_lockdown_drilled", "restore_drill_passed", ): if field in payload: @@ -811,6 +812,7 @@ def metadata_template() -> dict[str, Any]: "openbao_trial_material_exposed": False, "openbao_compromise_response_complete": False, "openbao_unseal_keys_rotated": False, + "openbao_emergency_lockdown_drilled": False, "root_token_disposition": "", "restore_drill_passed": False, "cleanup_complete": False, @@ -1339,6 +1341,7 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: trial_exposed = yes(data, "openbao_trial_material_exposed") response_complete = yes(data, "openbao_compromise_response_complete") keys_rotated = yes(data, "openbao_unseal_keys_rotated") + lockdown_drilled = yes(data, "openbao_emergency_lockdown_drilled") openbao_direct_taint = openbao_trial_taint(data, "direct") openbao_downstream_taint = openbao_trial_taint(data, "downstream") @@ -1357,6 +1360,12 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: rotate_status = "blocked" rotate_location = "Record the key-compromise condition or schedule a normal rotation first." + lockdown_status = "done" if lockdown_drilled else "todo" + lockdown_location = "Requires an unsealed OpenBao instance and a token with root or sudo on sys/seal." + if not initialized: + lockdown_status = "blocked" + lockdown_location = "OpenBao is not recorded as unsealed; sealing is only meaningful while it is serving requests." + return [ add_taint( { @@ -1382,6 +1391,18 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: }, openbao_downstream_taint if trial_exposed and not response_complete else {}, ), + add_taint( + { + "name": "Emergency lock-down", + "description": "Seal Railiance OpenBao to stop access to stored OpenBao assets until a later unseal quorum reopens it.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": lockdown_location, + "state": lockdown_status, + }, + openbao_downstream_taint if initialized else {}, + ), ] @@ -1391,6 +1412,7 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: trial_exposed = yes(data, "openbao_trial_material_exposed") response_complete = yes(data, "openbao_compromise_response_complete") keys_rotated = yes(data, "openbao_unseal_keys_rotated") + lockdown_drilled = yes(data, "openbao_emergency_lockdown_drilled") openbao_direct_taint = openbao_trial_taint(data, "direct") openbao_downstream_taint = openbao_trial_taint(data, "downstream") @@ -1418,6 +1440,20 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: rotate_status = "blocked" rotate_reason = "Record exposure or schedule a normal rotation before generating new shares." + lockdown_status = "done" if lockdown_drilled else "todo" + lockdown_reason = "Emergency lock-down drill is recorded." if lockdown_drilled else "Seals OpenBao immediately. Use only during an emergency or scheduled drill." + if not initialized: + lockdown_status = "blocked" + lockdown_reason = "OpenBao must be unsealed before emergency seal changes availability." + seal_command = ( + "printf 'OpenBao token: ' >&2\n" + "read -rs OPENBAO_TOKEN\n" + "printf '\\n' >&2\n" + "printf '%s\\n' \"$OPENBAO_TOKEN\" | kubectl exec -i -n openbao openbao-0 -- " + "sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao operator seal'\n" + "unset OPENBAO_TOKEN" + ) + return [ add_taint( { @@ -1479,6 +1515,36 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: }, openbao_downstream_taint if trial_exposed and not response_complete else {}, ), + add_taint( + { + "name": "Emergency seal OpenBao", + "description": "Prompt locally for an OpenBao token and seal Railiance OpenBao without placing the token on the command line.", + "status": lockdown_status, + "status_reason": lockdown_reason, + "command": seal_command, + }, + openbao_downstream_taint if initialized else {}, + ), + add_taint( + { + "name": "Confirm sealed status", + "description": "Check that Railiance OpenBao reports Sealed true after an emergency seal.", + "status": "todo" if initialized else "blocked", + "status_reason": "Run after emergency seal; expect Sealed true." if initialized else "OpenBao must be initialized before status confirms lock-down.", + "command": "kubectl exec -n openbao openbao-0 -- bao status", + }, + openbao_downstream_taint if initialized else {}, + ), + add_taint( + { + "name": "Record emergency lock-down drill", + "description": "Non-secret metadata checkbox after an emergency seal drill or real lock-down is confirmed.", + "status": lockdown_status, + "status_reason": lockdown_reason, + "command": "Use the checkbox: Emergency lock-down drill recorded", + }, + openbao_downstream_taint if initialized else {}, + ), ] @@ -1490,24 +1556,36 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: runbook_rows = runbook_payloads(data) artifact_rows = artifact_payloads(data) return [ + { + "key": "intro", + "name": "Introduction & Actors", + "status": "ok", + "reason": "NetKingdom purpose, global actors, and supported custody shapes are visible.", + }, + { + "key": "subsystems", + "name": "Subsystems & Scopes", + "status": "ok" if all(row["state"] in {"ok", "set"} for row in subsystem_rows) else "err", + "reason": "Subsystems have install/access evidence." if all(row["state"] != "nil" for row in subsystem_rows) else "Complete subsystem setup fields and confirmations.", + }, { "key": "roles", "name": "Roles & Responsibilities", "status": "ok" if role_ok else "set", "reason": "All active bootstrap roles have a designated email." if role_ok else "Assign an email to each active bootstrap role.", }, - { - "key": "subsystems", - "name": "Subsystems & Scope", - "status": "ok" if all(row["state"] in {"ok", "set"} for row in subsystem_rows) else "err", - "reason": "Subsystems have install/access evidence." if all(row["state"] != "nil" for row in subsystem_rows) else "Complete subsystem setup fields and confirmations.", - }, { "key": "integrations", "name": "Integration & Tests", "status": "ok" if all(row["state"] == "ok" for row in integration_rows[:4]) else "set", "reason": "Identity and OpenBao preflight checks are done." if all(row["state"] == "ok" for row in integration_rows[:4]) else "Run or confirm the remaining integration checks.", }, + { + "key": "artifacts", + "name": "Artefacts & Locations", + "status": "ok" if all(row["state"] != "nil" for row in artifact_rows[:10]) else "set", + "reason": "Core custody artefacts have locations or confirmations." if all(row["state"] != "nil" for row in artifact_rows[:10]) else "Record missing artefact locations and confirmations.", + }, { "key": "runbooks", "name": "Usecases & Runbooks", @@ -1515,10 +1593,10 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "reason": "Runbook states are recorded." if all(row["state"] in {"done", "blocked"} for row in runbook_rows) else "Review active runbooks and record non-secret outcomes.", }, { - "key": "artifacts", - "name": "Artefacts & Locations", - "status": "ok" if all(row["state"] != "nil" for row in artifact_rows[:10]) else "set", - "reason": "Core custody artefacts have locations or confirmations." if all(row["state"] != "nil" for row in artifact_rows[:10]) else "Record missing artefact locations and confirmations.", + "key": "terminology", + "name": "Terminology & Patterns", + "status": "ok", + "reason": "Shared NetKingdom security terms and patterns are documented for operators.", }, ] @@ -1815,6 +1893,18 @@ def ui_html() -> str: background: var(--paper); padding: 18px; } + #approval-form { + display: flex; + flex-direction: column; + } + .workflow-section[data-section="intro"] { order: 1; } + .workflow-section[data-section="subsystems"] { order: 2; } + .workflow-section[data-section="roles"] { order: 3; } + .workflow-section[data-section="integrations"] { order: 4; } + .workflow-section[data-section="artifacts"] { order: 5; } + .workflow-section[data-section="runbooks"] { order: 6; } + .workflow-section[data-section="terminology"] { order: 7; } + .workflow-actions { order: 8; } .panel + .panel { margin-top: 18px; } h2 { margin: 0 0 14px; @@ -2179,8 +2269,26 @@ def ui_html() -> str:
+
+ 1. Introduction & Actorsnil +
Loading introduction gate.
+

NetKingdom is the operating frame for accountable digital sovereignty: identity, custody, secrets, approvals, recovery, and emergency control are made explicit before subsystems are trusted with live assets.

+
+
role
King / CEO
Ultimate accountable authority for the kingdom, final custody intent, and existential risk decisions.
Global actor
Always present
+
role
Guardian / CSO
Owns security posture, emergency lock-down readiness, incident response, and custody-risk review.
Global actor
Required for quorum setups
+
role
Armorer / CTO
Owns technical implementation, platform architecture, automation, and operational correctness.
Global actor
Required for quorum setups
+
role
Steward / COO
Owns process reliability, onboarding, offboarding, runbook hygiene, and day-to-day operating cadence.
Global actor
Five-person setup
+
role
Treasurer / CFO
Owns financial authorization, asset inventory, economic risk, and continuity of valuable holdings.
Global actor
Five-person setup
+
+
+
AOne Person KingdomKing / CEO only. Best for early bootstrap and solo custody; fast but has no human quorum.
+
BThree Person KingdomKing / CEO plus two others. Decisions and access control target a two-of-three quorum.
+
CFive Person KingdomAll global actors active. Decisions and access control target a three-of-five quorum.
+
+
+
- 1. Roles & Responsibilitiesnil + 3. Roles & Responsibilitiesnil
Loading role gate.

Define who is accountable for each bootstrap role before touching subsystem-specific controls. Role chips in every record show the role name; hover them to see the designated email.

@@ -2228,7 +2336,7 @@ def ui_html() -> str:
- 2. Subsystems & Scopenil + 2. Subsystems & Scopesnil
Loading subsystem gate.

This section is about installing each subsystem and establishing initial user access. Integration checks come later.

@@ -2307,7 +2415,7 @@ def ui_html() -> str:
- 3. Integration & Testsnil + 4. Integration & Testsnil
Loading integration gate.

This section connects the subsystems and shows every console command as a copyable block. Commands still run outside this browser so secret output never enters the control surface.

@@ -2337,7 +2445,7 @@ def ui_html() -> str:
- 4. Usecases & Runbooksnil + 6. Usecases & Runbooksnil
Loading runbook gate.

Use these routines when the ceremony path changes, trial secrets are exposed, or custody material must be regenerated. The UI records only non-secret outcomes.

@@ -2345,6 +2453,7 @@ def ui_html() -> str: +
@@ -2375,13 +2484,34 @@ def ui_html() -> str: Approval phrase for selected strategy + + +
+ 7. Terminology & Patternsnil +
Loading terminology gate.
+

These terms apply across NetKingdom. Subsystems may have their own names, but the control surface keeps the cross-subsystem security pattern visible.

+
+
term
Subsystem
A bounded tool or service with its own admin surface, state, and security model.
Pattern
LLDAP, privacyIDEA, KeyCape, OpenBao
+
term
Artefact
A named credential, key, token, group, policy, packet, bundle, or evidence item with an owner and location.
Pattern
Track name, subsystem, role, location, state
+
term
Custody
The human and technical arrangement that controls who can access or recover critical material.
Pattern
Single king, two-of-three, three-of-five
+
term
Quorum
A threshold of actors or shares required before sensitive decisions or recovery actions can proceed.
Pattern
2 of 3, 3 of 5
+
term
Taint
A warning that work is downstream of a compromised, exposed, or trial-only artefact. Taint informs; it does not hard-block the operator.
Pattern
Keeps source reference visible
+
term
Break-glass
Emergency material that can restore control but is not part of normal daily operation.
Pattern
Retrieve, use, review, rotate if needed
+
term
Seal and unseal
For Railiance OpenBao, sealing stops access to stored OpenBao assets; unsealing requires the configured threshold of unseal shares.
Pattern
Emergency lock-down and reopening
+
term
Root token vs unseal shares
The Railiance OpenBao initial root token is a superuser API credential after unseal. Unseal shares control whether OpenBao can decrypt and serve requests at all.
Pattern
Different layers, different custody
+
term
Least privilege
Daily access should use scoped credentials; root, quorum, and break-glass paths remain exceptional and reviewable.
Pattern
Delegate, expire, audit
+
+
+ +
+

Actions

Waiting for local approval.
- +