diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index f941c13..d228b55 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -28,7 +28,7 @@ from typing import Any DEFAULT_STAGE = "S1 - Low-trust assembly" -STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5") +STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5", "S6") 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,13 +351,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, "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 "S6 - Reopen under custody" + if yes(data, "cleanup_complete") or yes(data, "openbao_oidc_admin_login_verified"): + return "S5 - Cleanup and hardening" + if yes(data, "openbao_initial_config_applied"): + return "S4 - Admin identity integration" if yes(data, "openbao_initialized"): return "S3 - OpenBao bootstrap" if yes(data, "king_credential_ready") or king_kit_ready(data): @@ -387,17 +385,23 @@ def stage_payloads(data: dict[str, Any]) -> list[dict[str, str]]: ( "S3", "OpenBao bootstrap", - "Initialize, unseal, configure OpenBao, then decide root-token disposition and prove restore.", - "Record root-token disposition and pass the restore drill.", + "Initialize, unseal, rotate trial material, configure OpenBao, and create a temporary admin bridge.", + "Bind normal OpenBao admin access to NetKingdom identity.", ), ( "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.", + "Admin identity integration", + "Register OpenBao with KeyCape and map verified NetKingdom admin claims to the platform-admin policy.", + "Verify OIDC-backed OpenBao admin login.", ), ( "S5", + "Cleanup and hardening", + "Retire bootstrap-era access, resolve taint, record root-token disposition, prove restore, and document residual risk.", + "Complete cleanup and mark the platform ready to reopen.", + ), + ( + "S6", "Reopen under custody", "Operate under the approved custody model with break-glass and recovery paths known.", "Review related workplans.", @@ -455,6 +459,21 @@ def build_gates(data: dict[str, Any]) -> list[Gate]: ), "Apply first auth, mount, and policy configuration; audit may be a declarative follow-up.", ), + Gate( + "KeyCape OpenBao client", + "done" if yes(data, "openbao_oidc_client_registered") else "blocked", + "Dedicated OpenBao OIDC client is registered in KeyCape with exact redirect URIs.", + ), + Gate( + "OpenBao OIDC auth", + "done" if yes(data, "openbao_oidc_auth_configured") else "blocked", + "OpenBao OIDC/JWT auth is configured against KeyCape and maps claims to policy.", + ), + Gate( + "OIDC admin login", + "done" if yes(data, "openbao_oidc_admin_login_verified") else "blocked", + "platform-root can obtain OpenBao platform-admin access through KeyCape/MFA.", + ), Gate( "Root-token disposition", "done" if data.get("root_token_disposition") in {"revoked", "offline-sealed"} else "blocked", @@ -519,6 +538,12 @@ def next_action( return "Approve custody strategy" if gate.name == "OpenBao preflight": return "Run OpenBao preflight" + if gate.name == "KeyCape OpenBao client": + return "Register OpenBao OIDC client" + if gate.name == "OpenBao OIDC auth": + return "Configure OpenBao OIDC auth" + if gate.name == "OIDC admin login": + return "Verify OpenBao OIDC admin login" if gate.name == "Root-token disposition": return "Record root-token disposition" if gate.name == "Restore drill": @@ -665,6 +690,9 @@ def merged_approval_metadata( "openbao_compromise_response_complete", "openbao_unseal_keys_rotated", "openbao_emergency_lockdown_drilled", + "openbao_oidc_client_registered", + "openbao_oidc_auth_configured", + "openbao_oidc_admin_login_verified", "restore_drill_passed", "cleanup_complete", "platform_reopened", @@ -885,6 +913,9 @@ def metadata_template() -> dict[str, Any]: "openbao_compromise_response_complete": False, "openbao_unseal_keys_rotated": False, "openbao_emergency_lockdown_drilled": False, + "openbao_oidc_client_registered": False, + "openbao_oidc_auth_configured": False, + "openbao_oidc_admin_login_verified": False, "root_token_disposition": "", "restore_drill_passed": False, "cleanup_complete": False, @@ -1154,6 +1185,48 @@ def integration_payloads(data: dict[str, Any]) -> list[dict[str, str]]: ] +def admin_identity_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + downstream_taint = openbao_trial_taint(data, "downstream") if yes(data, "openbao_initialized") else {} + return [ + add_taint( + { + "name": "OpenBao OIDC client", + "description": "Dedicated KeyCape client for OpenBao admin login with exact localhost callback URIs.", + "subsystem": "KeyCape", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": "clientId=openbao-admin; scopes=openid profile email groups", + "state": state_value(yes(data, "openbao_oidc_client_registered"), yes(data, "openbao_initial_config_applied")), + }, + downstream_taint, + ), + add_taint( + { + "name": "OpenBao OIDC auth method", + "description": "OpenBao trusts KeyCape discovery and maps the net-kingdom-admins group to the platform-admin policy.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "auth/keycape or equivalent OpenBao OIDC/JWT mount", + "state": state_value(yes(data, "openbao_oidc_auth_configured"), yes(data, "openbao_oidc_client_registered")), + }, + downstream_taint, + ), + add_taint( + { + "name": "OIDC admin login", + "description": "platform-root can obtain OpenBao platform-admin access through KeyCape MFA instead of a manually minted token.", + "subsystem": "KeyCape -> OpenBao", + "responsibility": "platform-root-custodian", + "email": role_email(data, "role_platform_custodian_email"), + "location": "bao login -method=oidc -path=keycape role=platform-admin", + "state": state_value(yes(data, "openbao_oidc_admin_login_verified"), yes(data, "openbao_oidc_auth_configured")), + }, + downstream_taint, + ), + ] + + def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]: public_key = extract_age_public_key(data.get("custodian_age_public_key")) state = bootstrap_secret_state() @@ -1675,6 +1748,7 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: role_ok = all(row["state"] != "nil" for row in role_rows[:5]) subsystem_rows = subsystem_payloads(data) integration_rows = integration_payloads(data) + admin_rows = admin_identity_payloads(data) artifact_rows = artifact_payloads(data) cleanup_done = yes(data, "cleanup_complete") reopened = yes(data, "platform_reopened") @@ -1703,6 +1777,12 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "status": "ok" if all(row["state"] == "ok" for row in integration_rows[:4]) else "set", "reason": "Identity and OpenBao preflight checks are done." if all(row["state"] == "ok" for row in integration_rows[:4]) else "Run or confirm the remaining integration checks.", }, + { + "key": "admin-identity", + "name": "Admin Identity Integration", + "status": "ok" if all(row["state"] == "ok" for row in admin_rows) else "set", + "reason": "OpenBao admin access is bound to NetKingdom OIDC claims." if all(row["state"] == "ok" for row in admin_rows) else "Configure and verify the KeyCape-backed OpenBao admin path.", + }, { "key": "artifacts", "name": "Artefacts & Locations", @@ -1766,6 +1846,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]: "roles": role_payloads(merged), "subsystems": subsystem_payloads(merged), "integrations": integration_payloads(merged), + "admin_identity_integrations": admin_identity_payloads(merged), "runbooks": runbook_payloads(merged), "artifacts": artifact_payloads(merged), "commands": command_payloads(merged), @@ -2018,7 +2099,7 @@ def ui_html() -> str: } .stage-rail { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; margin-top: 14px; } @@ -2073,11 +2154,12 @@ def ui_html() -> str: .workflow-section[data-section="subsystems"] { order: 2; } .workflow-section[data-section="roles"] { order: 3; } .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="handover"] { order: 7; } - .workflow-section[data-section="terminology"] { order: 8; } - .workflow-actions { order: 9; } + .workflow-section[data-section="admin-identity"] { order: 5; } + .workflow-section[data-section="artifacts"] { order: 6; } + .workflow-section[data-section="runbooks"] { order: 7; } + .workflow-section[data-section="handover"] { order: 8; } + .workflow-section[data-section="terminology"] { order: 9; } + .workflow-actions { order: 10; } .panel + .panel { margin-top: 18px; } h2 { margin: 0 0 14px; @@ -2619,8 +2701,25 @@ def ui_html() -> str:
+
+ 5. Admin Identity Integrationnil +
Loading admin identity gate.
+

This stage replaces manually minted OpenBao admin tokens as the normal path. KeyCape remains the identity issuer; OpenBao maps verified NetKingdom group claims to the existing platform-admin policy.

+
+ +
+ + + +
+
+
- 5. Artefacts & Locationsnil + 6. Artefacts & Locationsnil
Loading artefact gate.

This is the final overview of what has been established. Locations are references only; passwords, OTP seeds, age private keys, unseal shares, and root tokens are never recorded here.

@@ -2648,7 +2747,7 @@ def ui_html() -> str:
- 6. Usecases & Runbooksnil + 7. Usecases & Runbooksnil
Loading runbook gate.

This section contains reusable actions and runbook templates. Action cards are copyable command references without task status; runbook task state belongs to Integration & Tests or to the explicit confirmation gates.

@@ -2662,7 +2761,7 @@ def ui_html() -> str:
- 7. Final Handovernil + 8. Final Handovernil
Loading final handover gate.

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.

- 8. Terminology & Patternsnil + 9. Terminology & Patternsnil
Loading terminology gate.

These terms apply across NetKingdom. Subsystems may have their own names, but the control surface keeps the cross-subsystem security pattern visible.

@@ -2758,6 +2857,9 @@ def ui_html() -> str: "openbao_compromise_response_complete", "openbao_unseal_keys_rotated", "openbao_emergency_lockdown_drilled", + "openbao_oidc_client_registered", + "openbao_oidc_auth_configured", + "openbao_oidc_admin_login_verified", "root_token_disposition", "restore_drill_passed", "cleanup_complete", @@ -3020,6 +3122,7 @@ def ui_html() -> str: renderRecords("roles-records", data.roles); renderRecords("subsystems-records", data.subsystems); renderRecords("integrations-records", data.integrations); + renderRecords("admin-identity-records", data.admin_identity_integrations); renderRecords("runbooks-records", data.runbooks); renderRecords("artifacts-records", data.artifacts); renderCommands("command-list", data.commands); @@ -3072,6 +3175,9 @@ def ui_html() -> str: openbao_compromise_response_complete: document.getElementById("openbao_compromise_response_complete").checked, openbao_unseal_keys_rotated: document.getElementById("openbao_unseal_keys_rotated").checked, openbao_emergency_lockdown_drilled: document.getElementById("openbao_emergency_lockdown_drilled").checked, + openbao_oidc_client_registered: document.getElementById("openbao_oidc_client_registered").checked, + openbao_oidc_auth_configured: document.getElementById("openbao_oidc_auth_configured").checked, + openbao_oidc_admin_login_verified: document.getElementById("openbao_oidc_admin_login_verified").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, diff --git a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md index 1346648..61d1147 100644 --- a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md +++ b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md @@ -303,6 +303,12 @@ prompts for the bootstrap/root token without placing it on the command line and reminds the operator to store the emitted token through the approved secret path. +**2026-05-26:** Promoted the KeyCape-to-OpenBao admin path into its own stage +before cleanup and hardening. The control surface now has S4 Admin Identity +Integration with gates for the dedicated KeyCape OpenBao client, OpenBao +OIDC/JWT auth configuration, and MFA-backed OpenBao admin login verification; +cleanup and reopening move to S5/S6. + **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 @@ -345,7 +351,7 @@ roles later, but must be revocable without losing root custody. ```task id: NET-WP-0015-T06 -status: todo +status: in_progress priority: medium state_hub_task_id: "ef97f3cb-9792-4b9d-bd2b-8871d368a50f" ```