generated from coulomb/repo-seed
Close OpenBao OIDC admin bootstrap path
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user