generated from coulomb/repo-seed
Refine bootstrap responsibilities and command states
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user