Split OpenBao admin identity tasks
This commit is contained in:
parent
9dc7e140b8
commit
f3c8d70270
5 changed files with 292 additions and 23 deletions
|
|
@ -101,13 +101,27 @@ kubectl rollout restart deployment/keycape -n sso
|
||||||
## OIDC client registration
|
## OIDC client registration
|
||||||
|
|
||||||
Downstream applications are registered in the `clients:` block in
|
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
|
```bash
|
||||||
./create-secrets.sh # regenerates keycape-config Secret
|
./create-secrets.sh # regenerates keycape-config Secret
|
||||||
kubectl rollout restart deployment/keycape -n sso
|
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):
|
Example entry (public client, PKCE, for a SPA):
|
||||||
```yaml
|
```yaml
|
||||||
clients:
|
clients:
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,14 @@ clients:
|
||||||
allowedScopes: ["openid", "profile", "email", "groups"]
|
allowedScopes: ["openid", "profile", "email", "groups"]
|
||||||
grantTypes: ["authorization_code"]
|
grantTypes: ["authorization_code"]
|
||||||
clientType: "public"
|
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
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
# 3. At least one non-admin user exists in LLDAP
|
# 3. At least one non-admin user exists in LLDAP
|
||||||
# 4. Break-glass user exists and is in net-kingdom-admins
|
# 4. Break-glass user exists and is in net-kingdom-admins
|
||||||
# 5. privacyIDEA self-service portal reachable
|
# 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:
|
# Usage:
|
||||||
# chmod +x verify-t07.sh
|
# chmod +x verify-t07.sh
|
||||||
|
|
@ -174,6 +174,39 @@ if [[ -n "$KC_POD" ]]; then
|
||||||
else
|
else
|
||||||
warn "No OIDC clients registered — add clients to keycape/create-secrets.sh and re-run it"
|
warn "No OIDC clients registered — add clients to keycape/create-secrets.sh and re-run it"
|
||||||
fi
|
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
|
else
|
||||||
warn "Skipping client check — KeyCape not reachable in-cluster"
|
warn "Skipping client check — KeyCape not reachable in-cluster"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from typing import Any
|
||||||
DEFAULT_STAGE = "S1 - Low-trust assembly"
|
DEFAULT_STAGE = "S1 - Low-trust assembly"
|
||||||
STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5", "S6")
|
STAGE_ORDER = ("S1", "S2", "S3", "S4", "S5", "S6")
|
||||||
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
|
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
APPROVAL_PHRASE = "approve custody mode"
|
APPROVAL_PHRASE = "approve custody mode"
|
||||||
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
|
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
|
||||||
VALID_MFA_CLASSES = {"totp", "webauthn", "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_CLIENT_ID = "netkingdom-bootstrap-console"
|
||||||
OIDC_SCOPE = "openid profile email groups"
|
OIDC_SCOPE = "openid profile email groups"
|
||||||
OIDC_CODE_VERIFIER = "netkingdom-bootstrap-local-oidc-verifier-2026-v1"
|
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_PUBLIC_PREFIX = "age1"
|
||||||
AGE_PRIVATE_MARKER = "AGE-SECRET-KEY-1"
|
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
|
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:
|
def second_factor_ready(data: dict[str, Any]) -> bool:
|
||||||
return (
|
return (
|
||||||
data.get("mfa_class") in VALID_MFA_CLASSES
|
data.get("mfa_class") in VALID_MFA_CLASSES
|
||||||
|
|
@ -391,8 +411,8 @@ def stage_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||||
(
|
(
|
||||||
"S4",
|
"S4",
|
||||||
"Admin identity integration",
|
"Admin identity integration",
|
||||||
"Register OpenBao with KeyCape and map verified NetKingdom admin claims to the platform-admin policy.",
|
"Deploy the code-defined KeyCape client, map verified NetKingdom admin claims to OpenBao policy, and verify login.",
|
||||||
"Verify OIDC-backed OpenBao admin login.",
|
"Apply live KeyCape config, configure OpenBao OIDC auth, and verify admin login.",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"S5",
|
"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.",
|
"Apply first auth, mount, and policy configuration; audit may be a declarative follow-up.",
|
||||||
),
|
),
|
||||||
Gate(
|
Gate(
|
||||||
"KeyCape OpenBao client",
|
"KeyCape OpenBao client definition",
|
||||||
"done" if yes(data, "openbao_oidc_client_registered") else "blocked",
|
"done" if keycape_openbao_client_source_ready() else "blocked",
|
||||||
"Dedicated OpenBao OIDC client is registered in KeyCape with exact redirect URIs.",
|
"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(
|
Gate(
|
||||||
"OpenBao OIDC auth",
|
"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.",
|
"OpenBao OIDC/JWT auth is configured against KeyCape and maps claims to policy.",
|
||||||
),
|
),
|
||||||
Gate(
|
Gate(
|
||||||
"OIDC admin login",
|
"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.",
|
"platform-root can obtain OpenBao platform-admin access through KeyCape/MFA.",
|
||||||
),
|
),
|
||||||
Gate(
|
Gate(
|
||||||
|
|
@ -528,6 +571,12 @@ def next_action(
|
||||||
if data and yes(data, "openbao_init_output_produced") and not yes(data, "openbao_initialized"):
|
if data and yes(data, "openbao_init_output_produced") and not yes(data, "openbao_initialized"):
|
||||||
return "Run OpenBao unseal prompt"
|
return "Run OpenBao unseal prompt"
|
||||||
return "Run attended OpenBao init ceremony"
|
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
|
return gate.name
|
||||||
if gate.status == "blocked":
|
if gate.status == "blocked":
|
||||||
if gate.name == "King credential kit":
|
if gate.name == "King credential kit":
|
||||||
|
|
@ -538,10 +587,12 @@ def next_action(
|
||||||
return "Approve custody strategy"
|
return "Approve custody strategy"
|
||||||
if gate.name == "OpenBao preflight":
|
if gate.name == "OpenBao preflight":
|
||||||
return "Run OpenBao preflight"
|
return "Run OpenBao preflight"
|
||||||
if gate.name == "KeyCape OpenBao client":
|
if gate.name == "KeyCape OpenBao client definition":
|
||||||
return "Register OpenBao OIDC client"
|
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":
|
if gate.name == "OpenBao OIDC auth":
|
||||||
return "Configure OpenBao OIDC auth"
|
return "Run OpenBao OIDC auth setup"
|
||||||
if gate.name == "OIDC admin login":
|
if gate.name == "OIDC admin login":
|
||||||
return "Verify OpenBao OIDC admin login"
|
return "Verify OpenBao OIDC admin login"
|
||||||
if gate.name == "Root-token disposition":
|
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]]:
|
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 {}
|
downstream_taint = openbao_trial_taint(data, "downstream") if yes(data, "openbao_initialized") else {}
|
||||||
|
source_ready = keycape_openbao_client_source_ready()
|
||||||
return [
|
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(
|
add_taint(
|
||||||
{
|
{
|
||||||
"name": "OpenBao OIDC client",
|
"name": "openbao-admin client deployed",
|
||||||
"description": "Dedicated KeyCape client for OpenBao admin login with exact localhost callback URIs.",
|
"description": "The code-defined KeyCape client is applied to the live keycape-config Secret and KeyCape was restarted.",
|
||||||
"subsystem": "KeyCape",
|
"subsystem": "KeyCape",
|
||||||
"responsibility": "identity-admin",
|
"responsibility": "identity-admin",
|
||||||
"email": role_email(data, "role_identity_admin_email"),
|
"email": role_email(data, "role_identity_admin_email"),
|
||||||
"location": "clientId=openbao-admin; scopes=openid profile email groups",
|
"location": "clientId=openbao-admin; CLI redirects localhost/127.0.0.1:8250",
|
||||||
"state": state_value(yes(data, "openbao_oidc_client_registered"), yes(data, "openbao_initial_config_applied")),
|
"state": state_value(yes(data, "openbao_oidc_client_registered"), source_ready and yes(data, "openbao_initial_config_applied")),
|
||||||
},
|
},
|
||||||
downstream_taint,
|
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]]:
|
def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||||
public_key = extract_age_public_key(data.get("custodian_age_public_key"))
|
public_key = extract_age_public_key(data.get("custodian_age_public_key"))
|
||||||
state = bootstrap_secret_state()
|
state = bootstrap_secret_state()
|
||||||
|
|
@ -1808,7 +2013,7 @@ def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
|
||||||
"key": "admin-identity",
|
"key": "admin-identity",
|
||||||
"name": "Admin Identity Integration",
|
"name": "Admin Identity Integration",
|
||||||
"status": "ok" if all(row["state"] == "ok" for row in admin_rows) else "set",
|
"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",
|
"key": "artifacts",
|
||||||
|
|
@ -1874,6 +2079,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
|
||||||
"subsystems": subsystem_payloads(merged),
|
"subsystems": subsystem_payloads(merged),
|
||||||
"integrations": integration_payloads(merged),
|
"integrations": integration_payloads(merged),
|
||||||
"admin_identity_integrations": admin_identity_payloads(merged),
|
"admin_identity_integrations": admin_identity_payloads(merged),
|
||||||
|
"admin_identity_commands": admin_identity_command_payloads(merged),
|
||||||
"runbooks": runbook_payloads(merged),
|
"runbooks": runbook_payloads(merged),
|
||||||
"artifacts": artifact_payloads(merged),
|
"artifacts": artifact_payloads(merged),
|
||||||
"commands": command_payloads(merged),
|
"commands": command_payloads(merged),
|
||||||
|
|
@ -2731,16 +2937,17 @@ def ui_html() -> str:
|
||||||
<details class="panel workflow-section" data-section="admin-identity" open>
|
<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>
|
<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>
|
<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>
|
<div id="admin-identity-records" class="record-list"></div>
|
||||||
<ul class="spec-list" style="margin-top: 14px;">
|
<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>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>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>Operator deployment: apply the updated KeyCape config to live <code>keycape-config</code> and restart KeyCape if the live client is missing.</li>
|
||||||
<li>Verify <code>platform-root</code> can complete MFA-backed OpenBao login before root-token disposition and cleanup.</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>
|
</ul>
|
||||||
|
<div id="admin-identity-command-list" class="command-list"></div>
|
||||||
<div class="choice-list">
|
<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_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.</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>
|
<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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -3150,6 +3357,7 @@ def ui_html() -> str:
|
||||||
renderRecords("subsystems-records", data.subsystems);
|
renderRecords("subsystems-records", data.subsystems);
|
||||||
renderRecords("integrations-records", data.integrations);
|
renderRecords("integrations-records", data.integrations);
|
||||||
renderRecords("admin-identity-records", data.admin_identity_integrations);
|
renderRecords("admin-identity-records", data.admin_identity_integrations);
|
||||||
|
renderCommands("admin-identity-command-list", data.admin_identity_commands);
|
||||||
renderRecords("runbooks-records", data.runbooks);
|
renderRecords("runbooks-records", data.runbooks);
|
||||||
renderRecords("artifacts-records", data.artifacts);
|
renderRecords("artifacts-records", data.artifacts);
|
||||||
renderCommands("command-list", data.commands);
|
renderCommands("command-list", data.commands);
|
||||||
|
|
|
||||||
|
|
@ -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
|
revoked. Downstream work remains visibly tainted until derived access paths
|
||||||
are reviewed and the compromise response is explicitly recorded complete.
|
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
|
**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
|
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
|
the custodian public age recipient, a derived fingerprint, and a non-secret
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue