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

@@ -101,13 +101,27 @@ kubectl rollout restart deployment/keycape -n sso
## OIDC client registration
Downstream applications are registered in the `clients:` block in
`keycape/create-secrets.sh`. After editing:
`keycape/create-secrets.sh`. The NetKingdom bootstrap console and Railiance
OpenBao admin CLI clients are code-defined there; operators should not create
those clients manually in a separate UI. After changing the block:
```bash
./create-secrets.sh # regenerates keycape-config Secret
kubectl rollout restart deployment/keycape -n sso
```
The `openbao-admin` client is intentionally a public PKCE client for the
current local operator CLI flow. It registers the OpenBao CLI callback URIs:
```text
http://localhost:8250/oidc/callback
http://127.0.0.1:8250/oidc/callback
```
OpenBao browser UI callbacks are not registered yet because Railiance OpenBao
currently has public ingress disabled. Add exact UI callback URIs only after
the OpenBao UI exposure model is explicitly designed.
Example entry (public client, PKCE, for a SPA):
```yaml
clients:

View File

@@ -121,6 +121,14 @@ clients:
allowedScopes: ["openid", "profile", "email", "groups"]
grantTypes: ["authorization_code"]
clientType: "public"
- clientId: "openbao-admin"
displayName: "Railiance OpenBao Admin CLI"
redirectUris:
- "http://localhost:8250/oidc/callback"
- "http://127.0.0.1:8250/oidc/callback"
allowedScopes: ["openid", "profile", "email", "groups"]
grantTypes: ["authorization_code"]
clientType: "public"
EOF
)

View File

@@ -9,7 +9,7 @@
# 3. At least one non-admin user exists in LLDAP
# 4. Break-glass user exists and is in net-kingdom-admins
# 5. privacyIDEA self-service portal reachable
# 6. KeyCape config has at least one OIDC client registered
# 6. KeyCape config has OIDC clients, including openbao-admin
#
# Usage:
# chmod +x verify-t07.sh
@@ -174,6 +174,39 @@ if [[ -n "$KC_POD" ]]; then
else
warn "No OIDC clients registered — add clients to keycape/create-secrets.sh and re-run it"
fi
OPENBAO_CLIENT_CHECK=$(echo "$CONFIG" | python3 -c '
import sys
import yaml
cfg = yaml.safe_load(sys.stdin.read()) or {}
clients = cfg.get("clients") or []
target = next((client for client in clients if client.get("clientId") == "openbao-admin"), None)
if not target:
print("missing openbao-admin client")
raise SystemExit(2)
required_redirects = {
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback",
}
required_scopes = {"openid", "profile", "email", "groups"}
missing_redirects = sorted(required_redirects - set(target.get("redirectUris") or []))
missing_scopes = sorted(required_scopes - set(target.get("allowedScopes") or []))
if target.get("clientType") != "public":
print("openbao-admin clientType must be public for the current PKCE-only KeyCape profile")
raise SystemExit(3)
if missing_redirects:
print("openbao-admin missing redirect URI(s): " + ", ".join(missing_redirects))
raise SystemExit(4)
if missing_scopes:
print("openbao-admin missing scope(s): " + ", ".join(missing_scopes))
raise SystemExit(5)
print("openbao-admin client has local CLI redirects and required scopes")
' 2>/dev/null || echo "missing or invalid openbao-admin client")
if [[ "$OPENBAO_CLIENT_CHECK" == openbao-admin* ]]; then
pass "$OPENBAO_CLIENT_CHECK"
else
fail "$OPENBAO_CLIENT_CHECK"
fi
else
warn "Skipping client check — KeyCape not reachable in-cluster"
fi

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);

View File

@@ -315,6 +315,12 @@ initial-root-token taint clears after the exposed OpenBao root token is
revoked. Downstream work remains visibly tainted until derived access paths
are reviewed and the compromise response is explicitly recorded complete.
**2026-05-26:** Split Admin Identity Integration into development-owned
configuration and operator-owned integration work. The `openbao-admin` KeyCape
client is now code-defined in `sso-mfa/k8s/keycape/create-secrets.sh`, while
the UI action cards only ask the operator to apply live KeyCape config,
configure OpenBao with a protected token prompt, and verify MFA-backed login.
**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