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()
};

View File

@@ -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