diff --git a/tools/security-bootstrap-console/README.md b/tools/security-bootstrap-console/README.md index 5f77f89..4bd5153 100644 --- a/tools/security-bootstrap-console/README.md +++ b/tools/security-bootstrap-console/README.md @@ -92,6 +92,13 @@ Role, subsystem, integration, and artefact records use the same fields: States are `nil`, `set`, `err`, and `ok`. Role chips expose the designated email as hover text. +Responsibility assignments are edited through the **Change responsibilities** +foldout. Editing enables local Save/Cancel actions; Save writes only non-secret +role metadata and Cancel restores the last loaded values. Command cards use +`blocked`, `todo`, `redo`, and `done` to show whether an operator command is +available, needs to be run, should be repeated after a state change, or has +already succeeded. + The UI is a guide and approval surface, not the identity provider. Current lightweight-mode credential placement is: diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index e11ec9f..e6655f1 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -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:
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.
-