Split OpenBao admin identity tasks

This commit is contained in:
2026-05-26 02:13:55 +02:00
parent 9dc7e140b8
commit f3c8d70270
5 changed files with 292 additions and 23 deletions

View File

@@ -30,6 +30,7 @@ from typing import Any
DEFAULT_STAGE = "S1 - Low-trust assembly"
STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5", "S6")
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
REPO_ROOT = Path(__file__).resolve().parents[2]
APPROVAL_PHRASE = "approve custody mode"
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
VALID_MFA_CLASSES = {"totp", "webauthn", "hardware-token"}
@@ -45,6 +46,12 @@ KEYCAPE_ISSUER = "https://kc.coulomb.social"
OIDC_CLIENT_ID = "netkingdom-bootstrap-console"
OIDC_SCOPE = "openid profile email groups"
OIDC_CODE_VERIFIER = "netkingdom-bootstrap-local-oidc-verifier-2026-v1"
KEYCAPE_OPENBAO_CLIENT_ID = "openbao-admin"
KEYCAPE_OPENBAO_CLIENT_CONFIG = REPO_ROOT / "sso-mfa/k8s/keycape/create-secrets.sh"
KEYCAPE_OPENBAO_CLIENT_REDIRECTS = (
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback",
)
AGE_PUBLIC_PREFIX = "age1"
AGE_PRIVATE_MARKER = "AGE-SECRET-KEY-1"
@@ -94,6 +101,19 @@ def yes(data: dict[str, Any], key: str) -> bool:
return data.get(key) is True
def keycape_openbao_client_source_ready() -> bool:
try:
text = KEYCAPE_OPENBAO_CLIENT_CONFIG.read_text()
except OSError:
return False
required = [
f'clientId: "{KEYCAPE_OPENBAO_CLIENT_ID}"',
'allowedScopes: ["openid", "profile", "email", "groups"]',
]
required.extend(f'"{uri}"' for uri in KEYCAPE_OPENBAO_CLIENT_REDIRECTS)
return all(item in text for item in required)
def second_factor_ready(data: dict[str, Any]) -> bool:
return (
data.get("mfa_class") in VALID_MFA_CLASSES
@@ -391,8 +411,8 @@ def stage_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
(
"S4",
"Admin identity integration",
"Register OpenBao with KeyCape and map verified NetKingdom admin claims to the platform-admin policy.",
"Verify OIDC-backed OpenBao admin login.",
"Deploy the code-defined KeyCape client, map verified NetKingdom admin claims to OpenBao policy, and verify login.",
"Apply live KeyCape config, configure OpenBao OIDC auth, and verify admin login.",
),
(
"S5",
@@ -460,18 +480,41 @@ 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.",
"KeyCape OpenBao client definition",
"done" if keycape_openbao_client_source_ready() else "blocked",
"The non-secret openbao-admin client is defined in KeyCape source/config generation.",
),
Gate(
"KeyCape OpenBao client deployed",
(
"done"
if yes(data, "openbao_oidc_client_registered")
else "human"
if keycape_openbao_client_source_ready() and yes(data, "openbao_initial_config_applied")
else "blocked"
),
"Apply the code-defined client to the live KeyCape keycape-config Secret and restart KeyCape.",
),
Gate(
"OpenBao OIDC auth",
"done" if yes(data, "openbao_oidc_auth_configured") else "blocked",
(
"done"
if yes(data, "openbao_oidc_auth_configured")
else "human"
if yes(data, "openbao_oidc_client_registered")
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",
(
"done"
if yes(data, "openbao_oidc_admin_login_verified")
else "human"
if yes(data, "openbao_oidc_auth_configured")
else "blocked"
),
"platform-root can obtain OpenBao platform-admin access through KeyCape/MFA.",
),
Gate(
@@ -528,6 +571,12 @@ def next_action(
if data and yes(data, "openbao_init_output_produced") and not yes(data, "openbao_initialized"):
return "Run OpenBao unseal prompt"
return "Run attended OpenBao init ceremony"
if gate.name == "KeyCape OpenBao client deployed":
return "Apply KeyCape OpenBao client config"
if gate.name == "OpenBao OIDC auth":
return "Run OpenBao OIDC auth setup"
if gate.name == "OIDC admin login":
return "Verify OpenBao OIDC admin login"
return gate.name
if gate.status == "blocked":
if gate.name == "King credential kit":
@@ -538,10 +587,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 == "KeyCape OpenBao client definition":
return "Ship KeyCape OpenBao client definition"
if gate.name == "KeyCape OpenBao client deployed":
return "Apply KeyCape OpenBao client config"
if gate.name == "OpenBao OIDC auth":
return "Configure OpenBao OIDC auth"
return "Run OpenBao OIDC auth setup"
if gate.name == "OIDC admin login":
return "Verify OpenBao OIDC admin login"
if gate.name == "Root-token disposition":
@@ -1211,16 +1262,26 @@ 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 {}
source_ready = keycape_openbao_client_source_ready()
return [
{
"name": "openbao-admin client definition",
"description": "Code-defined KeyCape public PKCE client for Railiance OpenBao CLI login. This is development/configuration, not a manual registration step.",
"subsystem": "KeyCape source",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": "sso-mfa/k8s/keycape/create-secrets.sh",
"state": state_value(source_ready),
},
add_taint(
{
"name": "OpenBao OIDC client",
"description": "Dedicated KeyCape client for OpenBao admin login with exact localhost callback URIs.",
"name": "openbao-admin client deployed",
"description": "The code-defined KeyCape client is applied to the live keycape-config Secret and KeyCape was restarted.",
"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")),
"location": "clientId=openbao-admin; CLI redirects localhost/127.0.0.1:8250",
"state": state_value(yes(data, "openbao_oidc_client_registered"), source_ready and yes(data, "openbao_initial_config_applied")),
},
downstream_taint,
),
@@ -1251,6 +1312,150 @@ def admin_identity_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
]
def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
source_ready = keycape_openbao_client_source_ready()
client_deployed = yes(data, "openbao_oidc_client_registered")
auth_configured = yes(data, "openbao_oidc_auth_configured")
login_verified = yes(data, "openbao_oidc_admin_login_verified")
initial_config_applied = yes(data, "openbao_initial_config_applied")
downstream_taint = openbao_trial_taint(data, "downstream") if yes(data, "openbao_initialized") else {}
def action(
name: str,
description: str,
status: str,
status_reason: str,
command: str,
taint: dict[str, str] | None = None,
) -> dict[str, str]:
return add_taint(
{
"name": name,
"description": description,
"status": status,
"status_reason": status_reason,
"command": command,
},
taint or {},
)
deploy_state = "done" if client_deployed else "todo" if source_ready and initial_config_applied else "blocked"
deploy_reason = "Live KeyCape is recorded as carrying the code-defined openbao-admin client."
if deploy_state == "todo":
deploy_reason = "Operator action: apply the already-shipped KeyCape config and restart KeyCape. No new client secret is created."
if deploy_state == "blocked":
deploy_reason = "Blocked until OpenBao initial configuration exists and the KeyCape client definition is present in source."
auth_state = "done" if auth_configured else "todo" if client_deployed else "blocked"
auth_reason = "OpenBao OIDC/JWT auth is recorded as configured."
if auth_state == "todo":
auth_reason = "Operator action: requires a root/sudo-capable OpenBao token at the hidden prompt; the token value is not recorded."
if auth_state == "blocked":
auth_reason = "Apply and confirm the live KeyCape openbao-admin client before configuring OpenBao auth."
login_state = "done" if login_verified else "todo" if auth_configured else "blocked"
login_reason = "OIDC-backed OpenBao platform-admin login is recorded as verified."
if login_state == "todo":
login_reason = "Human verification: complete the browser MFA flow and confirm the resulting token has platform-admin policy."
if login_state == "blocked":
login_reason = "Configure OpenBao OIDC auth before testing the login path."
deploy_command = (
"cd sso-mfa/k8s/keycape\n"
"./create-secrets.sh\n"
"kubectl rollout restart deployment/keycape -n sso\n"
"kubectl rollout status deployment/keycape -n sso --timeout=60s\n"
"cd ..\n"
"./verify-t07.sh"
)
oidc_config_inner = """bao auth enable -path=keycape oidc >/tmp/keycape-auth-enable.out 2>/tmp/keycape-auth-enable.err || {
if grep -q "path is already in use" /tmp/keycape-auth-enable.err; then
printf "auth/keycape already exists\\n" >&2
else
cat /tmp/keycape-auth-enable.err >&2
exit 1
fi
}
bao write auth/keycape/config \\
oidc_discovery_url="https://kc.coulomb.social" \\
oidc_client_id="openbao-admin" \\
oidc_client_secret="" \\
default_role="platform-admin"
cat >/tmp/openbao-platform-admin-role.json <<ROLE_JSON
{
"role_type": "oidc",
"user_claim": "sub",
"groups_claim": "groups",
"oidc_scopes": ["openid", "profile", "email", "groups"],
"allowed_redirect_uris": [
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback"
],
"bound_claims": {
"groups": ["net-kingdom-admins"]
},
"claim_mappings": {
"email": "email",
"preferred_username": "username",
"groups": "groups"
},
"policies": ["platform-admin"],
"ttl": "1h"
}
ROLE_JSON
bao write auth/keycape/role/platform-admin @/tmp/openbao-platform-admin-role.json
rm -f /tmp/openbao-platform-admin-role.json /tmp/keycape-auth-enable.out /tmp/keycape-auth-enable.err"""
configure_command = (
"kubectl exec -it -n openbao openbao-0 -- sh -lc '\n"
" restore_tty() { stty echo 2>/dev/null || true; }\n"
" trap restore_tty EXIT INT TERM\n"
" printf \"OpenBao root/sudo token: \" >&2\n"
" stty -echo\n"
" read -r BAO_TOKEN\n"
" stty echo\n"
" printf \"\\n\" >&2\n"
" export BAO_TOKEN\n"
f"{oidc_config_inner}\n"
" unset BAO_TOKEN\n"
"'"
)
login_command = (
"# Terminal 1: keep a local OpenBao API port open while testing.\n"
"kubectl -n openbao port-forward svc/openbao-active 8200:8200\n\n"
"# Terminal 2: run the OIDC login and verify the policy on the returned token.\n"
"export BAO_ADDR=http://127.0.0.1:8200\n"
"bao login -method=oidc -path=keycape role=platform-admin\n"
"bao token lookup"
)
return [
action(
"Apply code-defined KeyCape OpenBao client",
"Deployment action for the non-secret openbao-admin client already present in source. Run this only if live KeyCape has not yet loaded the updated config.",
deploy_state,
deploy_reason,
deploy_command,
downstream_taint if yes(data, "openbao_initialized") else {},
),
action(
"Configure OpenBao OIDC auth",
"Create or update the auth/keycape mount and platform-admin role so KeyCape group claims map to OpenBao platform-admin policy.",
auth_state,
auth_reason,
configure_command,
downstream_taint if yes(data, "openbao_initialized") else {},
),
action(
"Verify OIDC-backed OpenBao admin login",
"Start a local port-forward, complete the KeyCape MFA browser flow, and verify the returned OpenBao token before checking the confirmation box.",
login_state,
login_reason,
login_command,
downstream_taint if yes(data, "openbao_initialized") else {},
),
]
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()
@@ -1808,7 +2013,7 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
"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.",
"reason": "OpenBao admin access is bound to NetKingdom OIDC claims." if all(row["state"] == "ok" for row in admin_rows) else "Run only the remaining operator cards: live KeyCape deploy, protected OpenBao auth setup, or login verification.",
},
{
"key": "artifacts",
@@ -1874,6 +2079,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
"subsystems": subsystem_payloads(merged),
"integrations": integration_payloads(merged),
"admin_identity_integrations": admin_identity_payloads(merged),
"admin_identity_commands": admin_identity_command_payloads(merged),
"runbooks": runbook_payloads(merged),
"artifacts": artifact_payloads(merged),
"commands": command_payloads(merged),
@@ -2731,16 +2937,17 @@ def ui_html() -> str:
<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>
<p class="notice">This stage replaces manually minted OpenBao admin tokens as the normal path. Development-owned client definitions are shipped in source; operator-owned cards below apply live config, use protected OpenBao prompts, or verify login.</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>
<li>Development/config: <code>openbao-admin</code> is defined in <code>sso-mfa/k8s/keycape/create-secrets.sh</code>; no manual KeyCape registration is expected.</li>
<li>Operator deployment: apply the updated KeyCape config to live <code>keycape-config</code> and restart KeyCape if the live client is missing.</li>
<li>Protected OpenBao step: configure <code>auth/keycape</code> with a hidden root/sudo token prompt, then verify <code>platform-root</code> can complete MFA-backed login.</li>
</ul>
<div id="admin-identity-command-list" class="command-list"></div>
<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_client_registered" type="checkbox"><span><strong>KeyCape OpenBao client deployed</strong><span>The code-defined <code>openbao-admin</code> client is present in live KeyCape after config apply/restart.</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. The setup command uses a hidden token prompt.</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>
@@ -3150,6 +3357,7 @@ def ui_html() -> str:
renderRecords("subsystems-records", data.subsystems);
renderRecords("integrations-records", data.integrations);
renderRecords("admin-identity-records", data.admin_identity_integrations);
renderCommands("admin-identity-command-list", data.admin_identity_commands);
renderRecords("runbooks-records", data.runbooks);
renderRecords("artifacts-records", data.artifacts);
renderCommands("command-list", data.commands);