generated from coulomb/repo-seed
Add bootstrap stage rail
This commit is contained in:
@@ -28,6 +28,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_STAGE = "S1 - Low-trust assembly"
|
DEFAULT_STAGE = "S1 - Low-trust assembly"
|
||||||
|
STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5")
|
||||||
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
|
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
|
||||||
APPROVAL_PHRASE = "approve custody mode"
|
APPROVAL_PHRASE = "approve custody mode"
|
||||||
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
|
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:
|
def derive_stage(data: dict[str, Any]) -> str:
|
||||||
if yes(data, "platform_reopened"):
|
if yes(data, "platform_reopened"):
|
||||||
return "S5 - Reopen under custody"
|
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"
|
return "S4 - Cleanup and hardening"
|
||||||
if yes(data, "openbao_initialized"):
|
if yes(data, "openbao_initialized"):
|
||||||
return "S3 - OpenBao bootstrap"
|
return "S3 - OpenBao bootstrap"
|
||||||
@@ -360,6 +365,63 @@ def derive_stage(data: dict[str, Any]) -> str:
|
|||||||
return DEFAULT_STAGE
|
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]:
|
def build_gates(data: dict[str, Any]) -> list[Gate]:
|
||||||
return [
|
return [
|
||||||
Gate(
|
Gate(
|
||||||
@@ -408,6 +470,11 @@ def build_gates(data: dict[str, Any]) -> list[Gate]:
|
|||||||
"done" if yes(data, "cleanup_complete") else "blocked",
|
"done" if yes(data, "cleanup_complete") else "blocked",
|
||||||
"Bootstrap-era credentials, databases, and access paths reviewed.",
|
"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"
|
return "Run restore drill"
|
||||||
if gate.name == "Cleanup and rotation":
|
if gate.name == "Cleanup and rotation":
|
||||||
return "Complete handover cleanup"
|
return "Complete handover cleanup"
|
||||||
|
if gate.name == "Platform reopen":
|
||||||
|
return "Reopen platform under custody"
|
||||||
return "Review related workplans"
|
return "Review related workplans"
|
||||||
|
|
||||||
|
|
||||||
@@ -597,6 +666,8 @@ def merged_approval_metadata(
|
|||||||
"openbao_unseal_keys_rotated",
|
"openbao_unseal_keys_rotated",
|
||||||
"openbao_emergency_lockdown_drilled",
|
"openbao_emergency_lockdown_drilled",
|
||||||
"restore_drill_passed",
|
"restore_drill_passed",
|
||||||
|
"cleanup_complete",
|
||||||
|
"platform_reopened",
|
||||||
):
|
):
|
||||||
if field in payload:
|
if field in payload:
|
||||||
data[field] = payload[field] is True
|
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)
|
subsystem_rows = subsystem_payloads(data)
|
||||||
integration_rows = integration_payloads(data)
|
integration_rows = integration_payloads(data)
|
||||||
artifact_rows = artifact_payloads(data)
|
artifact_rows = artifact_payloads(data)
|
||||||
|
cleanup_done = yes(data, "cleanup_complete")
|
||||||
|
reopened = yes(data, "platform_reopened")
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"key": "intro",
|
"key": "intro",
|
||||||
@@ -1595,6 +1668,18 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
|||||||
"status": "ok",
|
"status": "ok",
|
||||||
"reason": "Reusable actions and runbook templates are available; execution state is tracked by Integration & Tests and explicit confirmations.",
|
"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",
|
"key": "terminology",
|
||||||
"name": "Terminology & Patterns",
|
"name": "Terminology & Patterns",
|
||||||
@@ -1625,6 +1710,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"metadata_path": str(metadata_path),
|
"metadata_path": str(metadata_path),
|
||||||
"stage": derive_stage(merged),
|
"stage": derive_stage(merged),
|
||||||
|
"stage_steps": stage_payloads(merged),
|
||||||
"next_action": next_action(gates, kit_validation(merged), merged),
|
"next_action": next_action(gates, kit_validation(merged), merged),
|
||||||
"gates": [gate_payload(gate) for gate in gates],
|
"gates": [gate_payload(gate) for gate in gates],
|
||||||
"key_custody_gates": [gate_payload(gate) for gate in key_custody_validation(merged)],
|
"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;
|
line-height: 1.25;
|
||||||
overflow-wrap: anywhere;
|
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 {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.72fr);
|
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="integrations"] { order: 4; }
|
||||||
.workflow-section[data-section="artifacts"] { order: 5; }
|
.workflow-section[data-section="artifacts"] { order: 5; }
|
||||||
.workflow-section[data-section="runbooks"] { order: 6; }
|
.workflow-section[data-section="runbooks"] { order: 6; }
|
||||||
.workflow-section[data-section="terminology"] { order: 7; }
|
.workflow-section[data-section="handover"] { order: 7; }
|
||||||
.workflow-actions { order: 8; }
|
.workflow-section[data-section="terminology"] { order: 8; }
|
||||||
|
.workflow-actions { order: 9; }
|
||||||
.panel + .panel { margin-top: 18px; }
|
.panel + .panel { margin-top: 18px; }
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
@@ -2240,7 +2363,7 @@ def ui_html() -> str:
|
|||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
header { padding: 18px; }
|
header { padding: 18px; }
|
||||||
main { padding: 16px; }
|
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; }
|
.record-row { grid-template-columns: 1fr; }
|
||||||
.metric {
|
.metric {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
@@ -2270,6 +2393,7 @@ def ui_html() -> str:
|
|||||||
<span class="value"><code id="metadata-path">Loading</code></span>
|
<span class="value"><code id="metadata-path">Loading</code></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="stage-rail" id="stage-rail" aria-label="Bootstrap stages"></section>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<form id="approval-form">
|
<form id="approval-form">
|
||||||
@@ -2490,8 +2614,22 @@ def ui_html() -> str:
|
|||||||
<div id="runbook-command-list" class="command-list"></div>
|
<div id="runbook-command-list" class="command-list"></div>
|
||||||
</details>
|
</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>
|
<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>
|
<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>
|
<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">
|
<div class="record-list">
|
||||||
@@ -2574,7 +2712,9 @@ def ui_html() -> str:
|
|||||||
"openbao_unseal_keys_rotated",
|
"openbao_unseal_keys_rotated",
|
||||||
"openbao_emergency_lockdown_drilled",
|
"openbao_emergency_lockdown_drilled",
|
||||||
"root_token_disposition",
|
"root_token_disposition",
|
||||||
"restore_drill_passed"
|
"restore_drill_passed",
|
||||||
|
"cleanup_complete",
|
||||||
|
"platform_reopened"
|
||||||
];
|
];
|
||||||
const responsibilityFields = [
|
const responsibilityFields = [
|
||||||
"setup_operator",
|
"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) {
|
function makeStateBadge(state) {
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
const value = state || "nil";
|
const value = state || "nil";
|
||||||
@@ -2801,6 +2965,7 @@ def ui_html() -> str:
|
|||||||
document.getElementById("stage").textContent = data.stage;
|
document.getElementById("stage").textContent = data.stage;
|
||||||
document.getElementById("next-action").textContent = data.next_action;
|
document.getElementById("next-action").textContent = data.next_action;
|
||||||
document.getElementById("metadata-path").textContent = data.metadata_path;
|
document.getElementById("metadata-path").textContent = data.metadata_path;
|
||||||
|
renderStageRail(data.stage_steps);
|
||||||
renderGates("gates", data.gates);
|
renderGates("gates", data.gates);
|
||||||
renderGates("key-gates", data.key_custody_gates);
|
renderGates("key-gates", data.key_custody_gates);
|
||||||
renderGates("kit-gates", data.kit_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,
|
openbao_emergency_lockdown_drilled: document.getElementById("openbao_emergency_lockdown_drilled").checked,
|
||||||
root_token_disposition: document.getElementById("root_token_disposition").value,
|
root_token_disposition: document.getElementById("root_token_disposition").value,
|
||||||
restore_drill_passed: document.getElementById("restore_drill_passed").checked,
|
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,
|
approval_phrase: document.getElementById("approval_phrase").value,
|
||||||
approved_by: document.getElementById("setup_operator").value.trim()
|
approved_by: document.getElementById("setup_operator").value.trim()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -278,6 +278,12 @@ removed duplicate OpenBao status/unseal cards from the stateful Integration
|
|||||||
command list, and restored Artefacts & Locations above Usecases & Runbooks in
|
command list, and restored Artefacts & Locations above Usecases & Runbooks in
|
||||||
the workflow.
|
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
|
**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
|
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
|
the custodian public age recipient, a derived fingerprint, and a non-secret
|
||||||
|
|||||||
Reference in New Issue
Block a user