From d39dbe14b8c3c6f74723b0c4747884c997cb2f9c Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 25 May 2026 23:36:45 +0200 Subject: [PATCH] Add bootstrap stage rail --- .../security_bootstrap_console.py | 179 +++++++++++++++++- ...-custody-and-openbao-identity-bootstrap.md | 6 + 2 files changed, 179 insertions(+), 6 deletions(-) diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index 6358e50..fb0e495 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -28,6 +28,7 @@ from typing import Any DEFAULT_STAGE = "S1 - Low-trust assembly" +STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5") DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json") APPROVAL_PHRASE = "approve custody mode" VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"} @@ -351,7 +352,11 @@ def custody_mode_reason(data: dict[str, Any]) -> str: def derive_stage(data: dict[str, Any]) -> str: if yes(data, "platform_reopened"): return "S5 - Reopen under custody" - if yes(data, "cleanup_complete"): + if ( + yes(data, "openbao_initial_config_applied") + and data.get("root_token_disposition") in {"revoked", "offline-sealed"} + and yes(data, "restore_drill_passed") + ): return "S4 - Cleanup and hardening" if yes(data, "openbao_initialized"): return "S3 - OpenBao bootstrap" @@ -360,6 +365,63 @@ def derive_stage(data: dict[str, Any]) -> str: return DEFAULT_STAGE +def stage_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + current = derive_stage(data).split(" - ", 1)[0] + try: + current_index = STAGE_ORDER.index(current) + except ValueError: + current_index = 0 + rows = [ + ( + "S1", + "Low-trust assembly", + "Install and connect subsystems using low-trust setup accounts and non-secret metadata.", + "Complete the king credential kit.", + ), + ( + "S2", + "King credential preparation", + "Create platform-root, enroll MFA, approve custody strategy, and prepare recovery material.", + "Run OpenBao preflight and init under the selected custody strategy.", + ), + ( + "S3", + "OpenBao bootstrap", + "Initialize, unseal, configure OpenBao, then decide root-token disposition and prove restore.", + "Record root-token disposition and pass the restore drill.", + ), + ( + "S4", + "Cleanup and hardening", + "Rotate or retire bootstrap-era access, resolve taint, review audit, and document residual risk.", + "Complete cleanup and mark the platform ready to reopen.", + ), + ( + "S5", + "Reopen under custody", + "Operate under the approved custody model with break-glass and recovery paths known.", + "Review related workplans.", + ), + ] + payload = [] + for index, (stage_id, name, description, next_step) in enumerate(rows): + status = "pending" + if index < current_index: + status = "done" + elif index == current_index: + status = "active" + payload.append( + { + "id": stage_id, + "name": name, + "description": description, + "next": next_step, + "status": status, + } + ) + return payload + + def build_gates(data: dict[str, Any]) -> list[Gate]: return [ Gate( @@ -408,6 +470,11 @@ def build_gates(data: dict[str, Any]) -> list[Gate]: "done" if yes(data, "cleanup_complete") else "blocked", "Bootstrap-era credentials, databases, and access paths reviewed.", ), + Gate( + "Platform reopen", + "done" if yes(data, "platform_reopened") else "blocked", + "Final operator confirmation that the platform is reopened under custody.", + ), ] @@ -458,6 +525,8 @@ def next_action( return "Run restore drill" if gate.name == "Cleanup and rotation": return "Complete handover cleanup" + if gate.name == "Platform reopen": + return "Reopen platform under custody" return "Review related workplans" @@ -597,6 +666,8 @@ def merged_approval_metadata( "openbao_unseal_keys_rotated", "openbao_emergency_lockdown_drilled", "restore_drill_passed", + "cleanup_complete", + "platform_reopened", ): if field in payload: data[field] = payload[field] is True @@ -1558,6 +1629,8 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: subsystem_rows = subsystem_payloads(data) integration_rows = integration_payloads(data) artifact_rows = artifact_payloads(data) + cleanup_done = yes(data, "cleanup_complete") + reopened = yes(data, "platform_reopened") return [ { "key": "intro", @@ -1595,6 +1668,18 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "status": "ok", "reason": "Reusable actions and runbook templates are available; execution state is tracked by Integration & Tests and explicit confirmations.", }, + { + "key": "handover", + "name": "Final Handover", + "status": "ok" if reopened else "set" if cleanup_done else "err", + "reason": ( + "Platform is marked reopened under custody." + if reopened + else "Cleanup is complete; confirm the platform has reopened under custody." + if cleanup_done + else "Complete cleanup, taint response, and hardening before reopening." + ), + }, { "key": "terminology", "name": "Terminology & Patterns", @@ -1625,6 +1710,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]: return { "metadata_path": str(metadata_path), "stage": derive_stage(merged), + "stage_steps": stage_payloads(merged), "next_action": next_action(gates, kit_validation(merged), merged), "gates": [gate_payload(gate) for gate in gates], "key_custody_gates": [gate_payload(gate) for gate in key_custody_validation(merged)], @@ -1883,6 +1969,42 @@ def ui_html() -> str: line-height: 1.25; overflow-wrap: anywhere; } + .stage-rail { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-top: 14px; + } + .stage-card { + min-width: 0; + border: 1px solid var(--soft-line); + border-radius: 6px; + background: var(--paper); + padding: 12px; + } + .stage-card.done { background: var(--ok); } + .stage-card.active { + border-color: var(--line); + box-shadow: inset 0 0 0 2px var(--hi); + } + .stage-card.pending { background: #ffffff; } + .stage-id { + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 12px; + text-transform: uppercase; + color: var(--muted); + } + .stage-name { + margin-top: 5px; + font-weight: 650; + line-height: 1.2; + } + .stage-description, .stage-next { + margin-top: 6px; + color: var(--muted); + font-size: 13px; + line-height: 1.35; + } .layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 0.72fr); @@ -1906,8 +2028,9 @@ def ui_html() -> str: .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; } + .workflow-section[data-section="handover"] { order: 7; } + .workflow-section[data-section="terminology"] { order: 8; } + .workflow-actions { order: 9; } .panel + .panel { margin-top: 18px; } h2 { margin: 0 0 14px; @@ -2240,7 +2363,7 @@ def ui_html() -> str: @media (max-width: 820px) { header { padding: 18px; } main { padding: 16px; } - .topline, .layout, .grid { grid-template-columns: 1fr; } + .topline, .layout, .grid, .stage-rail { grid-template-columns: 1fr; } .record-row { grid-template-columns: 1fr; } .metric { border-right: 0; @@ -2270,6 +2393,7 @@ def ui_html() -> str: Loading +
@@ -2490,8 +2614,22 @@ def ui_html() -> str:
+
+ 7. Final Handovernil +
Loading final handover gate.
+

This is the line between trial/bootstrap and operating under custody. Mark these only after root-token disposition, restore proof, taint response, and cleanup have been handled outside this UI.

+
    +
  • Cleanup means bootstrap-era passwords, service tokens, temporary admin paths, trial OpenBao material, and plaintext secret exposure have been rotated, retired, reset, or explicitly accepted as residual risk.
  • +
  • Reopen means the platform is intentionally operated again under the selected custody strategy, with break-glass and restore paths known.
  • +
+
+ + +
+
+
- 7. Terminology & Patternsnil + 8. 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.

@@ -2574,7 +2712,9 @@ def ui_html() -> str: "openbao_unseal_keys_rotated", "openbao_emergency_lockdown_drilled", "root_token_disposition", - "restore_drill_passed" + "restore_drill_passed", + "cleanup_complete", + "platform_reopened" ]; const responsibilityFields = [ "setup_operator", @@ -2616,6 +2756,30 @@ def ui_html() -> str: } } + function renderStageRail(stages) { + const root = document.getElementById("stage-rail"); + root.replaceChildren(); + for (const stage of stages || []) { + const card = document.createElement("div"); + card.className = "stage-card " + stage.status; + card.title = stage.next || ""; + const id = document.createElement("div"); + id.className = "stage-id"; + id.textContent = stage.id + " / " + stage.status; + const name = document.createElement("div"); + name.className = "stage-name"; + name.textContent = stage.name; + const description = document.createElement("div"); + description.className = "stage-description"; + description.textContent = stage.description; + const next = document.createElement("div"); + next.className = "stage-next"; + next.textContent = stage.next; + card.append(id, name, description, next); + root.append(card); + } + } + function makeStateBadge(state) { const badge = document.createElement("span"); const value = state || "nil"; @@ -2801,6 +2965,7 @@ def ui_html() -> str: document.getElementById("stage").textContent = data.stage; document.getElementById("next-action").textContent = data.next_action; document.getElementById("metadata-path").textContent = data.metadata_path; + renderStageRail(data.stage_steps); renderGates("gates", data.gates); renderGates("key-gates", data.key_custody_gates); renderGates("kit-gates", data.kit_gates); @@ -2862,6 +3027,8 @@ def ui_html() -> str: openbao_emergency_lockdown_drilled: document.getElementById("openbao_emergency_lockdown_drilled").checked, root_token_disposition: document.getElementById("root_token_disposition").value, restore_drill_passed: document.getElementById("restore_drill_passed").checked, + cleanup_complete: document.getElementById("cleanup_complete").checked, + platform_reopened: document.getElementById("platform_reopened").checked, approval_phrase: document.getElementById("approval_phrase").value, approved_by: document.getElementById("setup_operator").value.trim() }; diff --git a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md index 5949569..28a287e 100644 --- a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md +++ b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md @@ -278,6 +278,12 @@ removed duplicate OpenBao status/unseal cards from the stateful Integration command list, and restored Artefacts & Locations above Usecases & Runbooks in the workflow. +**2026-05-25:** Added a five-stage visual stage rail and Final Handover +section to close the gap between OpenBao bootstrap and the final operating +state. The stage model now moves from S3 to S4 after OpenBao initial +configuration, root-token disposition, and restore drill are complete, then to +S5 only when the platform is explicitly reopened under custody. + **2026-05-24:** Stepped back from ad hoc secret rollout and added the custodian age-key bootstrap model to the control surface. The UI now records the custodian public age recipient, a derived fingerprint, and a non-secret