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
+