Add bootstrap stage rail

This commit is contained in:
2026-05-25 23:36:45 +02:00
parent cd043ca471
commit d39dbe14b8
2 changed files with 179 additions and 6 deletions

View File

@@ -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:
<span class="value"><code id="metadata-path">Loading</code></span>
</div>
</section>
<section class="stage-rail" id="stage-rail" aria-label="Bootstrap stages"></section>
<div class="layout">
<form id="approval-form">
@@ -2490,8 +2614,22 @@ def ui_html() -> str:
<div id="runbook-command-list" class="command-list"></div>
</details>
<details class="panel workflow-section" data-section="handover" open>
<summary><span class="summary-title">7. Final Handover</span><span class="state nil" data-section-state="handover">nil</span></summary>
<div class="section-gate" data-section-gate="handover">Loading final handover gate.</div>
<p class="notice">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.</p>
<ul class="spec-list">
<li>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.</li>
<li>Reopen means the platform is intentionally operated again under the selected custody strategy, with break-glass and restore paths known.</li>
</ul>
<div class="choice-list">
<label class="choice"><input id="cleanup_complete" type="checkbox"><span><strong>Cleanup and hardening complete</strong><span>Bootstrap-era credentials, databases, access paths, and tainted materials have been reviewed and handled.</span></span></label>
<label class="choice"><input id="platform_reopened" type="checkbox"><span><strong>Platform reopened under custody</strong><span>The operator accepts that the platform is now running under the approved custody model.</span></span></label>
</div>
</details>
<details class="panel workflow-section" data-section="terminology" open>
<summary><span class="summary-title">7. Terminology & Patterns</span><span class="state nil" data-section-state="terminology">nil</span></summary>
<summary><span class="summary-title">8. Terminology & Patterns</span><span class="state nil" data-section-state="terminology">nil</span></summary>
<div class="section-gate" data-section-gate="terminology">Loading terminology gate.</div>
<p class="notice">These terms apply across NetKingdom. Subsystems may have their own names, but the control surface keeps the cross-subsystem security pattern visible.</p>
<div class="record-list">
@@ -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()
};