Add OpenBao admin identity stage

This commit is contained in:
2026-05-26 01:17:42 +02:00
parent cfd8231849
commit 500e616202
2 changed files with 136 additions and 24 deletions

View File

@@ -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:
<div id="command-list" class="command-list"></div>
</details>
<details class="panel workflow-section" data-section="admin-identity" open>
<summary><span class="summary-title">5. Admin Identity Integration</span><span class="state nil" data-section-state="admin-identity">nil</span></summary>
<div class="section-gate" data-section-gate="admin-identity">Loading admin identity gate.</div>
<p class="notice">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.</p>
<div id="admin-identity-records" class="record-list"></div>
<ul class="spec-list" style="margin-top: 14px;">
<li>Register a dedicated KeyCape client such as <code>openbao-admin</code> with exact localhost OpenBao callback URIs and the <code>groups</code> scope.</li>
<li>Configure an OpenBao OIDC/JWT auth mount against <code>https://kc.coulomb.social</code> and bind <code>net-kingdom-admins</code> to <code>platform-admin</code>.</li>
<li>Verify <code>platform-root</code> can complete MFA-backed OpenBao login before root-token disposition and cleanup.</li>
</ul>
<div class="choice-list">
<label class="choice"><input id="openbao_oidc_client_registered" type="checkbox"><span><strong>OpenBao client registered in KeyCape</strong><span>The dedicated client exists with exact redirect URIs and allowed OIDC scopes.</span></span></label>
<label class="choice"><input id="openbao_oidc_auth_configured" type="checkbox"><span><strong>OpenBao OIDC auth configured</strong><span>OpenBao trusts KeyCape and maps NetKingdom admin claims to the platform-admin policy.</span></span></label>
<label class="choice"><input id="openbao_oidc_admin_login_verified" type="checkbox"><span><strong>OIDC admin login verified</strong><span>platform-root can obtain OpenBao admin access through KeyCape MFA without using a manually minted token.</span></span></label>
</div>
</details>
<details class="panel workflow-section" data-section="artifacts" open>
<summary><span class="summary-title">5. Artefacts & Locations</span><span class="state nil" data-section-state="artifacts">nil</span></summary>
<summary><span class="summary-title">6. Artefacts & Locations</span><span class="state nil" data-section-state="artifacts">nil</span></summary>
<div class="section-gate" data-section-gate="artifacts">Loading artefact gate.</div>
<p class="notice">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.</p>
<div id="artifacts-records" class="record-list"></div>
@@ -2648,7 +2747,7 @@ def ui_html() -> str:
</details>
<details class="panel workflow-section" data-section="runbooks" open>
<summary><span class="summary-title">6. Usecases & Runbooks</span><span class="state nil" data-section-state="runbooks">nil</span></summary>
<summary><span class="summary-title">7. Usecases & Runbooks</span><span class="state nil" data-section-state="runbooks">nil</span></summary>
<div class="section-gate" data-section-gate="runbooks">Loading runbook gate.</div>
<p class="notice">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.</p>
<div id="runbooks-records" class="record-list"></div>
@@ -2662,7 +2761,7 @@ def ui_html() -> str:
</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>
<summary><span class="summary-title">8. 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">
@@ -2676,7 +2775,7 @@ def ui_html() -> str:
</details>
<details class="panel workflow-section" data-section="terminology" open>
<summary><span class="summary-title">8. Terminology & Patterns</span><span class="state nil" data-section-state="terminology">nil</span></summary>
<summary><span class="summary-title">9. 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>
<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">
@@ -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,