Refine bootstrap responsibilities and command states

This commit is contained in:
2026-05-25 13:13:47 +02:00
parent 4982c92fb1
commit 976f399342
3 changed files with 218 additions and 42 deletions

View File

@@ -1129,16 +1129,67 @@ def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
]
def command_payloads() -> list[dict[str, str]]:
def command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
preflight_done = yes(data, "openbao_preflight_passed")
custody_approved = custody_mode_approved(data)
initialized = yes(data, "openbao_initialized")
root_disposed = data.get("root_token_disposition") in {"revoked", "offline-sealed"}
restore_done = yes(data, "restore_drill_passed")
status_state = "todo"
status_reason = "Run any time to inspect the current OpenBao deployment state."
if preflight_done:
status_state = "done"
status_reason = "Deployment and pre-init status were verified."
if initialized and not root_disposed:
status_state = "redo"
status_reason = "OpenBao changed during init/unseal; rerun status before root-token disposition."
preflight_state = "done" if preflight_done else "todo"
preflight_reason = "Safe preflight passed."
if not preflight_done:
preflight_reason = "Run after custody approval and before init."
if not custody_approved:
preflight_state = "blocked"
preflight_reason = "Approve the selected custody strategy first."
ceremony_state = "done" if initialized else "todo"
ceremony_reason = "Init/unseal ceremony has been recorded."
if not initialized:
ceremony_reason = "Run once, attended, after OpenBao preflight."
if not preflight_done:
ceremony_state = "blocked"
ceremony_reason = "OpenBao preflight must pass first."
config_state = "done" if root_disposed else "todo"
config_reason = "Initial configuration and root-token disposition are recorded."
if not root_disposed:
config_reason = "Configure OpenBao, then revoke or offline-seal the root token."
if not initialized:
config_state = "blocked"
config_reason = "OpenBao must be initialized and unsealed first."
verify_state = "done" if restore_done else "todo"
verify_reason = "Restore proof has been recorded."
if not restore_done:
verify_reason = "Verify post-unseal readiness, snapshot, and isolated restore."
if not initialized:
verify_state = "blocked"
verify_reason = "OpenBao must be initialized and unsealed first."
return [
{
"name": "OpenBao status",
"description": "Show pod, service, PVC, and seal/init status.",
"status": status_state,
"status_reason": status_reason,
"command": "make -C ../railiance-platform openbao-status",
},
{
"name": "OpenBao preflight",
"description": "Run safe status and verification checks. Does not initialize OpenBao.",
"status": preflight_state,
"status_reason": preflight_reason,
"command": (
"python3 tools/security-bootstrap-console/security_bootstrap_console.py "
"openbao-preflight --railiance-path ../railiance-platform --run"
@@ -1147,21 +1198,29 @@ def command_payloads() -> list[dict[str, str]]:
{
"name": "OpenBao init ceremony",
"description": "Creates real unseal shares and the initial root token. Run once, attended.",
"status": ceremony_state,
"status_reason": ceremony_reason,
"command": "kubectl exec -n openbao openbao-0 -- bao operator init -key-shares=3 -key-threshold=2",
},
{
"name": "OpenBao unseal prompt",
"description": "Enter unseal shares by prompt. Do not place shares on the command line.",
"status": ceremony_state,
"status_reason": ceremony_reason,
"command": "kubectl exec -n openbao openbao-0 -- bao operator unseal",
},
{
"name": "OpenBao initial configuration",
"description": "Apply first audit, auth, mount, and policy configuration after unseal.",
"status": config_state,
"status_reason": config_reason,
"command": "make -C ../railiance-platform openbao-configure-initial",
},
{
"name": "OpenBao post-unseal verification",
"description": "Verify filesystem and post-unseal readiness before live secrets move in.",
"status": verify_state,
"status_reason": verify_reason,
"command": "make -C ../railiance-platform openbao-verify-post-unseal",
},
]
@@ -1209,6 +1268,16 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
public_key = extract_age_public_key(metadata_view.get("custodian_age_public_key"))
metadata_view["custodian_age_public_key"] = public_key
metadata_view["custodian_age_public_key_fingerprint"] = age_public_key_fingerprint(public_key)
for role_key in (
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email",
):
if not metadata_view.get(role_key):
metadata_view[role_key] = role_email(merged, role_key)
return {
"metadata_path": str(metadata_path),
"stage": derive_stage(merged),
@@ -1221,7 +1290,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
"subsystems": subsystem_payloads(merged),
"integrations": integration_payloads(merged),
"artifacts": artifact_payloads(merged),
"commands": command_payloads(),
"commands": command_payloads(merged),
"bootstrap_secret_state": bootstrap_secret_state(),
"metadata": metadata_view,
"approval_phrase": APPROVAL_PHRASE,
@@ -1648,7 +1717,7 @@ def ui_html() -> str:
}
.record-row {
display: grid;
grid-template-columns: 62px minmax(170px, 1.2fr) minmax(120px, 0.7fr) minmax(150px, 0.8fr) minmax(150px, 1fr);
grid-template-columns: 58px minmax(170px, 1.25fr) minmax(130px, 0.72fr) minmax(150px, 1fr);
gap: 10px;
align-items: start;
border: 1px solid var(--soft-line);
@@ -1667,6 +1736,11 @@ def ui_html() -> str:
margin-top: 3px;
overflow-wrap: anywhere;
}
.record-context {
display: grid;
gap: 6px;
min-width: 0;
}
.state {
display: inline-flex;
justify-content: center;
@@ -1681,9 +1755,13 @@ def ui_html() -> str:
background: var(--warn);
}
.state.ok { background: var(--ok); }
.state.done { background: var(--ok); }
.state.set { background: var(--human); }
.state.redo { background: var(--human); }
.state.nil { background: #ffffff; }
.state.todo { background: var(--warn); }
.state.err { background: var(--bad); }
.state.blocked { background: var(--bad); }
.role-chip {
display: inline-flex;
max-width: 100%;
@@ -1826,40 +1904,47 @@ def ui_html() -> str:
<div class="section-gate" data-section-gate="roles">Loading role gate.</div>
<p class="notice">Define who is accountable for each bootstrap role before touching subsystem-specific controls. Role chips in every record show the role name; hover them to see the designated email.</p>
<div id="roles-records" class="record-list"></div>
<div class="grid">
<label class="field">
<span class="label">Setup operator</span>
<input id="setup_operator" type="text" autocomplete="off" title="Local operator handle used for non-secret progress records.">
</label>
<label class="field">
<span class="label">Notification contact</span>
<input id="notification_contact" type="text" autocomplete="off" title="Email for bootstrap notifications and lockout handling.">
</label>
<label class="field">
<span class="label">Setup operator email</span>
<input id="role_setup_operator_email" type="text" autocomplete="off" title="Designated user for the setup-operator role.">
</label>
<label class="field">
<span class="label">Platform-root custodian email</span>
<input id="role_platform_custodian_email" type="text" autocomplete="off" title="Designated user for the platform-root-custodian role.">
</label>
<label class="field">
<span class="label">Identity admin email</span>
<input id="role_identity_admin_email" type="text" autocomplete="off" title="Designated user for identity-admin during bootstrap.">
</label>
<label class="field">
<span class="label">OpenBao operator email</span>
<input id="role_openbao_operator_email" type="text" autocomplete="off" title="Designated user for the attended OpenBao ceremony.">
</label>
<label class="field">
<span class="label">Recovery custodian email</span>
<input id="role_recovery_custodian_email" type="text" autocomplete="off" title="Designated user for recovery material and bootstrap bundle custody.">
</label>
<label class="field">
<span class="label">Future quorum email</span>
<input id="role_future_quorum_email" type="text" autocomplete="off" title="Optional placeholder for later two-of-three custody migration.">
</label>
</div>
<details id="responsibility-editor" class="system-note">
<summary><span class="record-name">Change responsibilities</span><span class="state set" id="responsibility-edit-state">set</span></summary>
<div class="grid" style="margin-top: 14px;">
<label class="field">
<span class="label">Setup operator</span>
<input id="setup_operator" type="text" autocomplete="off" title="Local operator handle used for non-secret progress records.">
</label>
<label class="field">
<span class="label">Notification contact</span>
<input id="notification_contact" type="text" autocomplete="off" title="Email for bootstrap notifications and lockout handling.">
</label>
<label class="field">
<span class="label">Setup operator email</span>
<input id="role_setup_operator_email" type="text" autocomplete="off" title="Designated user for the setup-operator role.">
</label>
<label class="field">
<span class="label">Platform-root custodian email</span>
<input id="role_platform_custodian_email" type="text" autocomplete="off" title="Designated user for the platform-root-custodian role.">
</label>
<label class="field">
<span class="label">Identity admin email</span>
<input id="role_identity_admin_email" type="text" autocomplete="off" title="Designated user for identity-admin during bootstrap.">
</label>
<label class="field">
<span class="label">OpenBao operator email</span>
<input id="role_openbao_operator_email" type="text" autocomplete="off" title="Designated user for the attended OpenBao ceremony.">
</label>
<label class="field">
<span class="label">Recovery custodian email</span>
<input id="role_recovery_custodian_email" type="text" autocomplete="off" title="Designated user for recovery material and bootstrap bundle custody.">
</label>
<label class="field">
<span class="label">Future quorum email</span>
<input id="role_future_quorum_email" type="text" autocomplete="off" title="Optional placeholder for later two-of-three custody migration.">
</label>
</div>
<div class="actions">
<button class="secondary" id="responsibility-cancel-button" type="button" disabled title="Discard responsibility edits and close this foldout.">Cancel</button>
<button id="responsibility-save-button" type="button" disabled title="Save responsibility edits as non-secret local metadata and close this foldout.">Save</button>
</div>
</details>
</details>
<details class="panel workflow-section" data-section="subsystems" open>
@@ -2054,6 +2139,17 @@ def ui_html() -> str:
"root_token_disposition",
"restore_drill_passed"
];
const responsibilityFields = [
"setup_operator",
"notification_contact",
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email"
];
let currentMetadata = {};
function setMessage(text, kind) {
const element = document.getElementById("message");
@@ -2116,18 +2212,20 @@ def ui_html() -> str:
description.textContent = record.description;
identity.append(name, description);
const context = document.createElement("div");
context.className = "record-context";
const subsystem = document.createElement("div");
subsystem.className = "record-meta";
subsystem.textContent = record.subsystem;
const responsibility = document.createElement("div");
responsibility.append(makeRoleChip(record.responsibility, record.email));
context.append(subsystem, responsibility);
const location = document.createElement("div");
location.className = "record-meta";
location.textContent = record.location;
row.append(identity, subsystem, responsibility, location);
row.append(identity, context, location);
root.append(row);
}
}
@@ -2162,7 +2260,10 @@ def ui_html() -> str:
const description = document.createElement("div");
description.className = "record-description";
description.textContent = item.description;
title.append(name, description);
const statusReason = document.createElement("div");
statusReason.className = "record-description";
statusReason.textContent = item.status_reason || "";
title.append(name, description, statusReason);
const button = document.createElement("button");
button.className = "copy-button secondary";
@@ -2170,7 +2271,10 @@ def ui_html() -> str:
button.textContent = "Copy";
button.title = "Copy this console command to the clipboard.";
button.dataset.command = item.command;
head.append(title, button);
const commandActions = document.createElement("div");
commandActions.className = "inline-actions";
commandActions.append(makeStateBadge(item.status), button);
head.append(title, commandActions);
const code = document.createElement("code");
code.className = "command-code";
@@ -2203,6 +2307,29 @@ def ui_html() -> str:
syncConditionalHardware();
}
function fillResponsibilityFields(metadata) {
for (const id of responsibilityFields) {
const element = document.getElementById(id);
if (element) element.value = metadata[id] || "";
}
}
function responsibilityPayload() {
const payload = {};
for (const id of responsibilityFields) {
payload[id] = document.getElementById(id).value.trim();
}
return payload;
}
function setResponsibilityDirty(dirty) {
document.getElementById("responsibility-save-button").disabled = !dirty;
document.getElementById("responsibility-cancel-button").disabled = !dirty;
const badge = document.getElementById("responsibility-edit-state");
badge.className = "state " + (dirty ? "redo" : "set");
badge.textContent = dirty ? "redo" : "set";
}
function syncConditionalHardware() {
const source = document.getElementById("mfa_class");
const row = document.getElementById("hardware-storage-choice");
@@ -2227,7 +2354,9 @@ def ui_html() -> str:
renderRecords("integrations-records", data.integrations);
renderRecords("artifacts-records", data.artifacts);
renderCommands(data.commands);
fillForm(data.metadata || {});
currentMetadata = data.metadata || {};
fillForm(currentMetadata);
setResponsibilityDirty(false);
}
function approvalPayload() {
@@ -2275,6 +2404,40 @@ def ui_html() -> str:
document.getElementById("mfa_class").addEventListener("change", syncConditionalHardware);
document.getElementById("responsibility-editor").addEventListener("input", () => {
setResponsibilityDirty(true);
});
document.getElementById("responsibility-cancel-button").addEventListener("click", () => {
fillResponsibilityFields(currentMetadata);
setResponsibilityDirty(false);
document.getElementById("responsibility-editor").open = false;
});
document.getElementById("responsibility-save-button").addEventListener("click", async () => {
const button = document.getElementById("responsibility-save-button");
button.disabled = true;
try {
const response = await fetch("/api/save-progress", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(responsibilityPayload())
});
const result = await response.json();
if (!response.ok) {
setMessage("Save failed:\\n" + (result.errors || []).map((item) => "- " + item).join("\\n"), "error");
setResponsibilityDirty(true);
return;
}
await loadStatus();
document.getElementById("responsibility-editor").open = false;
setMessage("Responsibilities saved. No secrets were recorded.", "ok");
} catch (error) {
setMessage("Save failed: " + error.message, "error");
setResponsibilityDirty(true);
}
});
document.getElementById("command-list").addEventListener("click", async (event) => {
const button = event.target.closest(".copy-button");
if (!button) return;