diff --git a/sso-mfa/k8s/keycape/verify-openbao-client.sh b/sso-mfa/k8s/keycape/verify-openbao-client.sh index 1bc1085..55d58c7 100644 --- a/sso-mfa/k8s/keycape/verify-openbao-client.sh +++ b/sso-mfa/k8s/keycape/verify-openbao-client.sh @@ -13,6 +13,33 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" "$KUBECTL" get secret "$SECRET" -n "$NAMESPACE" -o json \ | python3 "$SCRIPT_DIR/openbao-client-config.py" verify +PUBLIC_URL="${KEYCAPE_PUBLIC_URL:-https://kc.coulomb.social}" +PUBLIC_AUTHORIZE_URL="${PUBLIC_URL%/}/authorize" +PUBLIC_PROBE_OUTPUT=$( + curl -sS -i -G "$PUBLIC_AUTHORIZE_URL" \ + --data-urlencode "client_id=openbao-admin" \ + --data-urlencode "redirect_uri=http://localhost:8250/oidc/callback" \ + --data-urlencode "response_type=code" \ + --data-urlencode "scope=openid profile email groups" \ + --data-urlencode "state=netkingdom-openbao-client-probe" \ + --data-urlencode "code_challenge=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ" \ + --data-urlencode "code_challenge_method=S256" \ + 2>&1 || true +) + +if grep -q '"unknown client_id"' <<<"$PUBLIC_PROBE_OUTPUT"; then + echo "[FAIL] $PUBLIC_AUTHORIZE_URL rejects openbao-admin with unknown client_id" >&2 + echo " Check DNS for kc.coulomb.social and ensure it reaches the KeyCape ingress that was patched." >&2 + exit 1 +fi +if ! grep -qE '^HTTP/[0-9.]+ 302 ' <<<"$PUBLIC_PROBE_OUTPUT"; then + echo "[FAIL] $PUBLIC_AUTHORIZE_URL did not return the expected OIDC redirect for openbao-admin" >&2 + echo " First response:" >&2 + sed -n '1,12p' <<<"$PUBLIC_PROBE_OUTPUT" >&2 + exit 1 +fi +echo "[PASS] public KeyCape authorize endpoint recognizes openbao-admin" + KC_POD=$("$KUBECTL" get pod -n "$NAMESPACE" \ -l app.kubernetes.io/name=keycape \ --field-selector=status.phase=Running \ diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index 6d25bed..ddff9ff 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -576,6 +576,8 @@ def next_action( if gate.name == "OpenBao OIDC auth": return "Run OpenBao OIDC auth setup" if gate.name == "OIDC admin login": + if data and yes(data, "openbao_oidc_auth_configured") and not yes(data, "openbao_oidc_admin_login_verified"): + return "Check KeyCape public route" return "Verify OpenBao OIDC admin login" return gate.name if gate.status == "blocked": @@ -594,6 +596,8 @@ def next_action( if gate.name == "OpenBao OIDC auth": return "Run OpenBao OIDC auth setup" if gate.name == "OIDC admin login": + if data and yes(data, "openbao_oidc_auth_configured") and not yes(data, "openbao_oidc_admin_login_verified"): + return "Check KeyCape public route" return "Verify OpenBao OIDC admin login" if gate.name == "Root-token disposition": return "Record root-token disposition" @@ -1361,6 +1365,7 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str] login_reason = "Configure OpenBao OIDC auth before testing the login path." keycape_dir = shlex.quote(str(KEYCAPE_OPENBAO_CLIENT_CONFIG.parent)) + kubectl_bin = "/home/worsch/.local/bin/kubectl" deploy_command = ( "bash <<'NETKINGDOM_KEYCAPE_APPLY'\n" "set -euo pipefail\n" @@ -1372,13 +1377,32 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str] "NETKINGDOM_KEYCAPE_APPLY\n" ) configure_command = f"bash {shlex.quote(str(KEYCAPE_OPENBAO_CLIENT_CONFIG.parent / 'configure-openbao-oidc.sh'))}" + public_route_state = "done" if login_verified else "todo" if auth_configured else "blocked" + public_route_reason = "The public KeyCape route has been proven by a completed OIDC-backed OpenBao login." + if public_route_state == "todo": + public_route_reason = "Operator action: confirm public DNS routes kc.coulomb.social to the patched KeyCape ingress and that /authorize recognizes openbao-admin." + if public_route_state == "blocked": + public_route_reason = "Configure OpenBao OIDC auth before probing the public login route." + public_route_command = ( + "bash <<'NETKINGDOM_KEYCAPE_PUBLIC_ROUTE'\n" + "set -euo pipefail\n" + f"cd {keycape_dir}\n" + f"KUBECTL={kubectl_bin} bash ./verify-openbao-client.sh\n" + "printf 'DNS addresses for kc.coulomb.social:\\n'\n" + "getent ahosts kc.coulomb.social | awk '{print $1}' | sort -u\n" + "printf 'KeyCape ingress address from Kubernetes:\\n'\n" + f"{kubectl_bin} get ingress keycape -n sso -o jsonpath='{{.status.loadBalancer.ingress[0].ip}}{{\"\\n\"}}'\n" + "NETKINGDOM_KEYCAPE_PUBLIC_ROUTE\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" + "# Terminal 1: bridge the browser callback to the bao CLI running in the OpenBao pod.\n" + "kubectl -n openbao port-forward pod/openbao-0 8250:8250\n\n" + "# Terminal 2: run the pod-bundled bao CLI, then copy the printed login URL into your local browser if needed.\n" + "kubectl exec -it -n openbao openbao-0 -- sh -lc '\n" + " export BAO_ADDR=http://127.0.0.1:8200\n" + " bao login -method=oidc -path=keycape role=platform-admin\n" + " bao token lookup\n" + "'" ) return [ @@ -1398,9 +1422,17 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str] configure_command, downstream_taint if yes(data, "openbao_initialized") else {}, ), + action( + "Check KeyCape public route for OpenBao", + "Verify that public DNS and ingress route kc.coulomb.social to the patched KeyCape instance before attempting the browser-based OpenBao login.", + public_route_state, + public_route_reason, + public_route_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.", + "Use the bao CLI already present in the OpenBao pod, bridge its localhost callback to your workstation, complete the KeyCape MFA browser flow, and verify the returned token before checking the confirmation box.", login_state, login_reason, login_command, @@ -2712,7 +2744,7 @@ def ui_html() -> str:
-
+
1. Introduction & Actorsnil
Loading introduction gate.

NetKingdom is the operating frame for accountable digital sovereignty: identity, custody, secrets, approvals, recovery, and emergency control are made explicit before subsystems are trusted with live assets.

@@ -2730,7 +2762,7 @@ def ui_html() -> str:
-
+
3. Roles & Responsibilitiesnil
Loading role gate.

Define who is accountable for each bootstrap role before touching subsystem-specific controls. Role chips in every record show the role name; hover them to see the designated email.

@@ -2778,7 +2810,7 @@ def ui_html() -> str:
-
+
2. Subsystems & Scopesnil
Loading subsystem gate.

This section is about installing each subsystem and establishing initial user access. Integration checks come later.

@@ -2857,7 +2889,7 @@ def ui_html() -> str:
-
+
4. Integration & Testsnil
Loading integration gate.

This section tracks stateful integration runbooks and gates. Status belongs to these tasks; reusable command-only actions live below in Usecases & Runbooks.

@@ -2887,7 +2919,7 @@ def ui_html() -> str:
-
+
5. Admin Identity Integrationnil
Loading admin identity gate.

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.

@@ -2905,7 +2937,7 @@ def ui_html() -> str:
-
+
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.

@@ -2933,7 +2965,7 @@ def ui_html() -> str:
-
+
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.

@@ -2947,7 +2979,7 @@ def ui_html() -> str:
-
+
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.

@@ -2961,7 +2993,7 @@ def ui_html() -> str:
-
+
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.

@@ -3182,6 +3214,10 @@ def ui_html() -> str: function renderSectionGates(gates) { for (const gate of gates || []) { + const section = document.querySelector(`details.workflow-section[data-section='${gate.key}']`); + if (section) { + section.open = gate.status !== "ok"; + } const badge = document.querySelector(`[data-section-state='${gate.key}']`); if (badge) { badge.className = "state " + gate.status;