Close OpenBao OIDC admin bootstrap path

This commit is contained in:
2026-06-01 21:20:53 +02:00
parent ed2cc17165
commit c48e076429
15 changed files with 374 additions and 86 deletions

View File

@@ -29,8 +29,8 @@ 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]
DEFAULT_METADATA_PATH = REPO_ROOT / ".local/security-bootstrap.json"
APPROVAL_PHRASE = "approve custody mode"
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
VALID_MFA_CLASSES = {"totp", "webauthn", "hardware-token"}
@@ -48,6 +48,7 @@ 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"
PRIVACYIDEA_REALM_REPAIR = REPO_ROOT / "sso-mfa/k8s/privacyidea/repair-realm-live.sh"
KEYCAPE_OPENBAO_CLIENT_REDIRECTS = (
"http://localhost:8250/oidc/callback",
"http://127.0.0.1:8250/oidc/callback",
@@ -1410,18 +1411,21 @@ def admin_identity_command_payloads(data: dict[str, Any]) -> list[dict[str, str]
refresh_pi_token_command = (
"set -euo pipefail\n"
f"cd {keycape_dir}\n"
f"KUBECTL={kubectl_bin} bash ./refresh-pi-token-live.sh platform-root\n"
f"KEYCAPE_PI_REALM=coulomb KUBECTL={kubectl_bin} bash ./refresh-pi-token-live.sh platform-root\n"
)
login_command = (
"# 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"
"# Terminal 1: start the pod-bundled bao CLI and wait until it prints the login URL.\n"
f"{kubectl_bin} 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"
" skip_browser=true listenaddress=0.0.0.0 callbackhost=127.0.0.1 port=8250\n"
" bao token lookup\n"
"'"
"'\n\n"
"# Terminal 2: start this only after Terminal 1 is waiting for OIDC authentication.\n"
f"{kubectl_bin} -n openbao port-forward pod/openbao-0 8250:8250\n\n"
"# Browser: open the URL printed by Terminal 1. If Firefox already failed at\n"
"# 127.0.0.1:8250 before the port-forward was ready, reload that callback URL\n"
"# or restart Terminal 1 to get a fresh login URL."
)
return [
@@ -1714,7 +1718,20 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
if not initial_config_applied:
restore_location = "Template: prepare now; execute after initial OpenBao configuration exists and before live secrets move in."
token_revocation_location = "Template: revoke the current helper token after attended checks, or revoke a disclosed token by accessor using a root/sudo-capable token."
if not initialized:
token_revocation_location = "Template: prepare now; execution needs an initialized OpenBao instance."
return [
{
"name": "privacyIDEA realm repair",
"description": "Recreate the coulomb realm, LLDAP resolver, and self-service policies without storing live passwords.",
"subsystem": "privacyIDEA",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": "Template: run the attended realm repair action, then enroll or re-enroll platform-root TOTP in the repaired realm.",
"state": "template",
},
add_taint(
{
"name": "Key material compromised",
@@ -1763,6 +1780,18 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
},
openbao_downstream_taint if initialized else {},
),
add_taint(
{
"name": "OpenBao token revocation",
"description": "Retire short-lived, temporary, or accidentally disclosed OpenBao tokens without putting token values on the local command line.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": token_revocation_location,
"state": "template",
},
openbao_downstream_taint if initialized else {},
),
]
@@ -1823,6 +1852,17 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
platform_admin_token_command = token_prompt_command(
"bao token create -policy=platform-admin -period=24h -orphan"
)
revoke_self_token_command = (
"kubectl exec -it -n openbao openbao-0 -- sh -lc '\n"
" export BAO_ADDR=http://127.0.0.1:8200\n"
" bao token lookup >/dev/null\n"
" bao token revoke -self\n"
"'"
)
revoke_accessor_command = interactive_token_command(
'printf "Token accessor to revoke: " >&2; read -r TARGET_ACCESSOR; '
'bao token revoke -accessor "$TARGET_ACCESSOR"; unset TARGET_ACCESSOR'
)
rotate_init_command = interactive_token_command(
"bao operator rotate-keys -init -key-shares=3 -key-threshold=2"
)
@@ -1880,8 +1920,14 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
"7. Destroy the isolated environment and record only non-secret evidence in this UI.\n"
"RESTORE_DRILL"
)
privacyidea_realm_command = f"bash {shlex.quote(str(PRIVACYIDEA_REALM_REPAIR))}"
return [
action(
"Repair privacyIDEA realm and self-service",
"Prompt for pi-admin and LLDAP bind passwords, recreate the coulomb realm, LLDAP resolver, self-enrollment policy, and passthrough bootstrap policy, then run T06 verification. The wrapper keeps passwords in a private temporary directory that is removed on exit.",
privacyidea_realm_command,
),
action(
"OpenBao status",
"Show seal, initialization, storage, and HA state for the OpenBao pod. This command does not require a token.",
@@ -1918,6 +1964,18 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
platform_admin_token_command,
downstream_taint,
),
action(
"Revoke current OpenBao token",
"Revoke the token currently stored in the OpenBao pod token helper, usually immediately after an attended verification flow.",
revoke_self_token_command,
downstream_taint,
),
action(
"Revoke OpenBao token by accessor",
"Prompt inside the pod for a root/sudo-capable token and the disclosed token accessor, then revoke that accessor without placing the token value on the local command line.",
revoke_accessor_command,
downstream_taint,
),
action(
"Start unseal-key rotation",
"Run once to start a new 3-share, threshold-2 rotation. If rotation is already in progress, do not rerun init; check status and submit existing shares.",
@@ -3288,7 +3346,6 @@ def ui_html() -> str:
button.type = "button";
button.textContent = "Copy";
button.title = "Copy this console command to the clipboard.";
button.dataset.command = item.command;
const commandActions = document.createElement("div");
commandActions.className = "inline-actions";
if (item.status) commandActions.append(makeStateBadge(item.status));
@@ -3477,7 +3534,9 @@ def ui_html() -> str:
document.addEventListener("click", async (event) => {
const button = event.target.closest(".copy-button");
if (!button) return;
const command = button.dataset.command || "";
const row = button.closest(".command-row");
const code = row ? row.querySelector(".command-code") : null;
const command = code ? code.textContent : "";
try {
await navigator.clipboard.writeText(command);
button.textContent = "Copied";
@@ -3642,6 +3701,8 @@ def make_ui_handler(metadata_path: Path) -> type[BaseHTTPRequestHandler]:
def serve_web_ui(args: argparse.Namespace) -> int:
metadata_path = args.metadata or DEFAULT_METADATA_PATH
if not metadata_path.exists():
write_metadata(metadata_path, metadata_template())
handler = make_ui_handler(metadata_path)
httpd = ThreadingHTTPServer((args.host, args.port), handler)
host = html.escape(args.host)
@@ -3736,6 +3797,9 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
metadata_commands = {"status", "validate-king-kit", "approve-custody-mode", "web-ui"}
if args.command in metadata_commands and args.metadata is None:
args.metadata = DEFAULT_METADATA_PATH
data = load_metadata(args.metadata)
if args.command == "status":