Files
net-kingdom/tools/security-bootstrap-console/security_bootstrap_console.py

2920 lines
123 KiB
Python
Executable File

#!/usr/bin/env python3
"""Non-secret security bootstrap console.
This tool is intentionally conservative. It prints status, gates, checklists,
and templates. It does not collect or store secret values, and it refuses to run
live OpenBao initialization.
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import html
import json
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timezone
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
DEFAULT_STAGE = "S1 - Low-trust assembly"
DEFAULT_METADATA_PATH = Path("/tmp/net-kingdom-security-bootstrap.json")
APPROVAL_PHRASE = "approve custody mode"
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
VALID_MFA_CLASSES = {"totp", "webauthn", "hardware-token"}
VALID_MFA_ENROLLMENT_SOURCES = {
"identity-provider",
"external-verifier",
"hardware-registration",
"deferred",
}
VALID_CUSTODY_MODES = {"temporary-single-king", "two-of-three-planned", "two-of-three-ready"}
CUSTODY_APPROVAL_MODES = {"temporary-single-king", "two-of-three-ready"}
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"
AGE_PUBLIC_PREFIX = "age1"
AGE_PRIVATE_MARKER = "AGE-SECRET-KEY-1"
@dataclass(frozen=True)
class Gate:
name: str
status: str
reason: str
def load_metadata(path: Path | None) -> dict[str, Any]:
if path is None or not path.exists():
return {}
try:
data = json.loads(path.read_text())
except json.JSONDecodeError as exc:
raise SystemExit(f"metadata is not valid JSON: {path}: {exc}") from exc
if not isinstance(data, dict):
raise SystemExit(f"metadata root must be an object: {path}")
return data
def write_metadata(path: Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_name(f".{path.name}.tmp")
tmp_path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
tmp_path.replace(path)
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def normalize_storage_classes(value: Any) -> list[str]:
if isinstance(value, str):
raw_values = [item.strip() for item in value.split(",")]
elif isinstance(value, list):
raw_values = [str(item).strip() for item in value]
else:
raw_values = []
values = [item for item in raw_values if item]
return sorted(set(values))
def yes(data: dict[str, Any], key: str) -> bool:
return data.get(key) is True
def second_factor_ready(data: dict[str, Any]) -> bool:
return (
data.get("mfa_class") in VALID_MFA_CLASSES
and yes(data, "mfa_enrolled_confirmed")
and data.get("mfa_enrollment_source") in VALID_MFA_ENROLLMENT_SOURCES - {"deferred"}
)
def second_factor_reason(data: dict[str, Any]) -> str:
if data.get("mfa_class") not in VALID_MFA_CLASSES:
return "Select TOTP, WebAuthn, or hardware-token."
if data.get("mfa_enrollment_source") == "deferred":
return "Deferred factor enrollment blocks live OpenBao custody."
if not yes(data, "mfa_enrolled_confirmed"):
return "Confirm the factor was enrolled with the authority that will verify it; record no seed."
if data.get("mfa_enrollment_source") not in VALID_MFA_ENROLLMENT_SOURCES:
return "Record the non-secret enrollment source."
return "Second factor enrollment is confirmed without recording seed material."
def identity_account_ready(data: dict[str, Any]) -> bool:
return (
yes(data, "identity_account_created")
and bool(data.get("identity_account_reference"))
and yes(data, "identity_group_confirmed")
and bool(data.get("identity_group_reference"))
)
def identity_account_reason(data: dict[str, Any]) -> str:
if not yes(data, "identity_account_created"):
return "Create the dedicated platform-root account in LLDAP first."
if not data.get("identity_account_reference"):
return "Record a non-secret account reference such as platform-root@lldap."
if not yes(data, "identity_group_confirmed"):
return "Confirm the account is assigned to the required LLDAP admin group."
if not data.get("identity_group_reference"):
return "Record the non-secret group reference such as net-kingdom-admins."
return "Dedicated identity account is recorded without storing its password."
def password_safe_ready(data: dict[str, Any]) -> bool:
return yes(data, "password_safe_confirmed")
def identity_login_ready(data: dict[str, Any]) -> bool:
return yes(data, "oidc_login_verified")
def recovery_material_ready(data: dict[str, Any]) -> bool:
return yes(data, "recovery_confirmed")
def recovery_material_reason(data: dict[str, Any]) -> str:
if recovery_material_ready(data):
return "Recovery references are prepared outside this UI."
return (
"Prepare password recovery, MFA recovery/re-enrollment, custodian age-key "
"recovery, and encrypted bootstrap bundle recovery references."
)
def custody_packet_ready(data: dict[str, Any]) -> bool:
return yes(data, "custody_packet_prepared")
def custody_packet_reason(data: dict[str, Any]) -> str:
if custody_packet_ready(data):
return "The offline ceremony packet is ready without recording secret values here."
return (
"Prepare the OpenBao ceremony packet: selected custody mode, recovery references, "
"share assignment slots, root-token disposition plan, and signature/date."
)
def metadata_secret_boundary_issue(data: dict[str, Any]) -> str:
state = bootstrap_secret_state()
if state["plaintext_secrets_present"]:
return "Plaintext bootstrap secrets directory is present; remove it before custody approval."
encoded = json.dumps(data, sort_keys=True)
secret_markers = (
AGE_PRIVATE_MARKER,
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
"OPENBAO_ROOT_TOKEN",
"VAULT_TOKEN",
)
for marker in secret_markers:
if marker in encoded:
return f"Metadata contains a secret-looking marker: {marker}."
return ""
def secret_boundary_ready(data: dict[str, Any]) -> bool:
return metadata_secret_boundary_issue(data) == ""
def secret_boundary_reason(data: dict[str, Any]) -> str:
issue = metadata_secret_boundary_issue(data)
if issue:
return issue
return "The control surface stores only non-secret references; no user attestation is required."
def extract_age_public_key(value: Any) -> str:
if value is None:
return ""
text = str(value).strip()
if AGE_PRIVATE_MARKER in text:
return ""
for token in text.replace(",", " ").split():
clean = token.strip().strip('"').strip("'")
if clean.startswith(AGE_PUBLIC_PREFIX):
return clean
return ""
def age_public_key_fingerprint(public_key: str) -> str:
if not public_key:
return ""
digest = hashlib.sha256(public_key.encode("utf-8")).hexdigest()
return f"sha256:{digest[:16]}"
def bootstrap_secret_state() -> dict[str, Any]:
root = Path.cwd()
bootstrap_dir = root / "sso-mfa" / "bootstrap"
encrypted_dir = bootstrap_dir / "secrets.enc"
plaintext_dir = bootstrap_dir / "secrets"
encrypted_files = sorted(encrypted_dir.glob("*/*.age")) if encrypted_dir.exists() else []
plaintext_files = sorted(path for path in plaintext_dir.glob("*/*") if path.is_file()) if plaintext_dir.exists() else []
return {
"bootstrap_dir": str(bootstrap_dir),
"encrypted_bundle_path": str(encrypted_dir),
"encrypted_bundle_exists": encrypted_dir.exists(),
"encrypted_file_count": len(encrypted_files),
"plaintext_secrets_path": str(plaintext_dir),
"plaintext_secrets_present": plaintext_dir.exists(),
"plaintext_file_count": len(plaintext_files),
}
def key_custody_validation(data: dict[str, Any]) -> list[Gate]:
public_key = extract_age_public_key(data.get("custodian_age_public_key"))
state = bootstrap_secret_state()
plaintext_present = bool(state["plaintext_secrets_present"])
return [
Gate(
"Custodian public key",
"done" if public_key and yes(data, "custodian_age_public_key_confirmed") else "blocked",
"Register the custodian age public key used to encrypt bootstrap bundles.",
),
Gate(
"Private key custody",
"done" if data.get("custodian_age_private_key_reference") and yes(data, "custodian_age_private_key_confirmed") else "blocked",
"Record only where the private key is held; never paste it into this UI.",
),
Gate(
"Encrypted bundle",
"done" if state["encrypted_bundle_exists"] and state["encrypted_file_count"] else "blocked",
"Encrypted bootstrap secrets should live under sso-mfa/bootstrap/secrets.enc/.",
),
Gate(
"Plaintext exposure",
"blocked" if plaintext_present else "done",
"Plaintext sso-mfa/bootstrap/secrets/ is present; shred it after any apply ceremony." if plaintext_present else "No plaintext bootstrap secrets directory is present.",
),
]
def kit_validation(data: dict[str, Any]) -> list[Gate]:
storage_classes = data.get("storage_classes", [])
if not isinstance(storage_classes, list):
storage_classes = []
storage_values = {str(item) for item in storage_classes}
custody_mode = data.get("custody_mode", "")
return [
Gate(
"Credential label",
"done" if data.get("credential_label") else "blocked",
"Use a dedicated label such as platform-root.",
),
Gate(
"Identity account",
"done" if identity_account_ready(data) else "blocked",
identity_account_reason(data),
),
Gate(
"Setup operator/contact",
"done" if data.get("setup_operator") and data.get("notification_contact") else "blocked",
"Record non-secret setup operator and notification contact.",
),
Gate(
"Storage class",
"done" if storage_values & VALID_STORAGE_CLASSES else "blocked",
"Select where the credential is held; hardware is optional policy, not a default requirement.",
),
Gate(
"Password safe storage",
"done" if password_safe_ready(data) else "blocked",
"Confirm the credential password is stored in the password safe without recording it here.",
),
Gate(
"Second factor",
"done" if second_factor_ready(data) else "blocked",
second_factor_reason(data),
),
Gate(
"Identity login path",
"done" if identity_login_ready(data) else "blocked",
"Verify the dedicated account can complete the NetKingdom login path.",
),
Gate(
"Custody strategy selected",
"done" if custody_mode in VALID_CUSTODY_MODES else "blocked",
"Choose how the OpenBao init ceremony will be controlled.",
),
Gate(
"Recovery material",
"done" if recovery_material_ready(data) else "blocked",
recovery_material_reason(data),
),
Gate(
"Custody packet",
"done" if custody_packet_ready(data) else "blocked",
custody_packet_reason(data),
),
Gate(
"Control-surface secret boundary",
"done" if secret_boundary_ready(data) else "blocked",
secret_boundary_reason(data),
),
]
def king_kit_ready(data: dict[str, Any]) -> bool:
gates = kit_validation(data)
return all(gate.status == "done" for gate in gates)
def custody_mode_approved(data: dict[str, Any]) -> bool:
return data.get("custody_mode") in CUSTODY_APPROVAL_MODES and yes(data, "custody_mode_approved")
def custody_mode_reason(data: dict[str, Any]) -> str:
mode = data.get("custody_mode")
if mode in CUSTODY_APPROVAL_MODES and yes(data, "custody_mode_approved"):
return "Approved for the next gate under the selected custody mode."
if mode == "two-of-three-planned":
return "Two-of-three is recorded as the target, but live init stays blocked until it is ready."
if mode in CUSTODY_APPROVAL_MODES and not yes(data, "custody_mode_approved"):
return "Strategy is selected; explicit approval is still pending."
return "Choose temporary-single-king or two-of-three-ready for live OpenBao custody."
def derive_stage(data: dict[str, Any]) -> str:
if yes(data, "platform_reopened"):
return "S5 - Reopen under custody"
if yes(data, "cleanup_complete"):
return "S4 - Cleanup and hardening"
if yes(data, "openbao_initialized"):
return "S3 - OpenBao bootstrap"
if yes(data, "king_credential_ready") or king_kit_ready(data):
return "S2 - King credential preparation"
return DEFAULT_STAGE
def build_gates(data: dict[str, Any]) -> list[Gate]:
return [
Gate(
"King credential kit",
"done" if yes(data, "king_credential_ready") or king_kit_ready(data) else "blocked",
"Dedicated king credential, second factor, and recovery storage.",
),
Gate(
"Custody strategy approval",
"done" if custody_mode_approved(data) else "blocked",
custody_mode_reason(data),
),
Gate(
"OpenBao preflight",
"done" if yes(data, "openbao_preflight_passed") else "blocked",
"Run safe Railiance OpenBao status and verification checks.",
),
Gate(
"OpenBao init ceremony",
"human" if not yes(data, "openbao_initialized") else "done",
"Human-attended ceremony only. This console will not run init.",
),
Gate(
"Root-token disposition",
"done" if data.get("root_token_disposition") in {"revoked", "offline-sealed"} else "blocked",
"Root token is revoked or sealed offline without recording value.",
),
Gate(
"Restore drill",
"done" if yes(data, "restore_drill_passed") else "blocked",
"Snapshot and isolated restore proof before live secrets.",
),
Gate(
"Cleanup and rotation",
"done" if yes(data, "cleanup_complete") else "blocked",
"Bootstrap-era credentials, databases, and access paths reviewed.",
),
]
def kit_next_action(kit_gates: list[Gate]) -> str:
labels = {
"Credential label": "Set credential label",
"Identity account": "Create platform-root account",
"Setup operator/contact": "Record setup contact",
"Storage class": "Select recovery storage",
"Password safe storage": "Confirm password safe entry",
"Second factor": "Enroll MFA factor",
"Identity login path": "Verify OIDC login",
"Custody strategy selected": "Select custody strategy",
"Recovery material": "Prepare recovery material",
"Custody packet": "Prepare custody packet",
"Control-surface secret boundary": "Confirm secret boundary",
}
for gate in kit_gates:
if gate.status != "done":
return labels.get(gate.name, "Define king credential kit")
return "Define king credential kit"
def next_action(
gates: list[Gate],
kit_gates: list[Gate] | None = None,
data: dict[str, Any] | None = None,
) -> str:
for gate in gates:
if gate.status == "human":
if gate.name == "OpenBao init ceremony":
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"
return gate.name
if gate.status == "blocked":
if gate.name == "King credential kit":
if kit_gates is not None:
return kit_next_action(kit_gates)
return "Define king credential kit"
if gate.name == "Custody strategy approval":
return "Approve custody strategy"
if gate.name == "OpenBao preflight":
return "Run OpenBao preflight"
if gate.name == "Root-token disposition":
return "Record root-token disposition"
if gate.name == "Restore drill":
return "Run restore drill"
if gate.name == "Cleanup and rotation":
return "Complete handover cleanup"
return "Review related workplans"
def print_status(data: dict[str, Any]) -> None:
merged = metadata_template()
merged.update(data)
gates = build_gates(merged)
key_gates = key_custody_validation(merged)
kit_gates = kit_validation(merged)
state = bootstrap_secret_state()
print("SECURITY BOOTSTRAP")
print("")
print("Stage")
print(derive_stage(merged))
print("")
print("Next safe action")
print(next_action(gates, kit_gates, merged))
print("")
print("Key custody")
public_key = extract_age_public_key(merged.get("custodian_age_public_key"))
print(f"- fingerprint: {age_public_key_fingerprint(public_key) or 'not registered'}")
print(f"- encrypted bundle files: {state['encrypted_file_count']} at {state['encrypted_bundle_path']}")
print(f"- plaintext secrets present: {state['plaintext_secrets_present']}")
for gate in key_gates:
print(f"- {gate.status}: {gate.name} - {gate.reason}")
print("")
print("Gates")
for gate in gates:
print(f"- {gate.status}: {gate.name} - {gate.reason}")
print("")
print("Available actions")
print("1. king-kit")
print("2. custody-packet")
print("3. openbao-preflight")
print("4. handover-checklist")
print("5. metadata-template")
print("6. approve-custody-mode")
print("7. web-ui")
print("")
print("Refusal boundary")
print("This console will not run bao operator init or collect secret values.")
def print_king_kit() -> None:
print("KING CREDENTIAL KIT")
print("")
rows = [
"Name the credential, for example platform-root.",
"Choose storage: password safe, offline packet, hardware-backed, or a combination.",
"Add a second factor: TOTP, WebAuthn, or hardware token.",
"Prepare recovery material without recording values in software.",
"Select custody mode: temporary-single-king, two-of-three-planned, or two-of-three-ready.",
"Print or prepare the offline custody packet.",
"Record only non-secret metadata.",
]
for index, row in enumerate(rows, start=1):
print(f"{index}. {row}")
def print_validate_king_kit(data: dict[str, Any]) -> int:
print("KING CREDENTIAL KIT VALIDATION")
print("")
if not data:
print("No metadata loaded. Use --metadata with a non-secret JSON file.")
print("Run metadata-template for the expected shape.")
return 2
gates = kit_validation(data)
for gate in gates:
print(f"- {gate.status}: {gate.name} - {gate.reason}")
print("")
if king_kit_ready(data) and custody_mode_approved(data):
print("Kit definition and custody-mode approval are complete.")
print("Live OpenBao init remains a separate human-attended ceremony.")
return 0
if king_kit_ready(data):
print("Kit definition is complete except custody-mode approval.")
print("Live OpenBao init is still blocked until T03 approves custody mode.")
return 0
print("Kit definition is incomplete.")
return 1
def merged_approval_metadata(
existing: dict[str, Any],
payload: dict[str, Any],
) -> dict[str, Any]:
data = metadata_template()
data.update(existing)
text_fields = (
"credential_label",
"bootstrap_mode",
"identity_account_home",
"identity_account_reference",
"identity_group_reference",
"custodian_age_private_key_reference",
"setup_operator",
"notification_contact",
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email",
"mfa_class",
"mfa_enrollment_source",
"mfa_enrollment_reference",
"custody_mode",
"root_token_disposition",
"notes",
)
for field in text_fields:
if field in payload and payload[field] is not None:
value = str(payload[field]).strip()
data[field] = "" if AGE_PRIVATE_MARKER in value else value
if "custodian_age_public_key" in payload:
data["custodian_age_public_key"] = extract_age_public_key(payload["custodian_age_public_key"])
if "storage_classes" in payload:
data["storage_classes"] = normalize_storage_classes(payload["storage_classes"])
for field in (
"custodian_age_public_key_confirmed",
"custodian_age_private_key_confirmed",
"recovery_confirmed",
"custody_packet_prepared",
"no_secret_capture_confirmed",
"mfa_enrolled_confirmed",
"identity_account_created",
"identity_group_confirmed",
"oidc_login_verified",
"password_safe_confirmed",
"openbao_preflight_passed",
"openbao_init_output_produced",
"openbao_initialized",
"openbao_trial_material_exposed",
"openbao_compromise_response_complete",
"openbao_unseal_keys_rotated",
"restore_drill_passed",
):
if field in payload:
data[field] = payload[field] is True
return data
def save_progress_metadata(existing: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
data = merged_approval_metadata(existing, payload)
data["metadata_updated_at"] = utc_now()
data["progress_scope"] = "Non-secret local bootstrap progress only."
return data
def validate_custody_approval(
data: dict[str, Any],
approval_phrase: str,
) -> list[str]:
errors: list[str] = []
mode = data.get("custody_mode")
if approval_phrase.strip().lower() != APPROVAL_PHRASE:
errors.append(f'Type "{APPROVAL_PHRASE}" to approve the selected custody strategy.')
if mode not in VALID_CUSTODY_MODES:
errors.append("Select a custody mode.")
elif mode not in CUSTODY_APPROVAL_MODES:
errors.append(
"two-of-three-planned is a target state, not live-init approval. "
"Use temporary-single-king now or two-of-three-ready when shares exist."
)
for gate in kit_validation(data):
if gate.name == "Custody strategy selected":
continue
if gate.status != "done":
errors.append(f"{gate.name}: {gate.reason}")
return errors
def approve_custody_metadata(
existing: dict[str, Any],
payload: dict[str, Any],
approval_phrase: str,
approver: str,
) -> tuple[dict[str, Any], list[str]]:
data = merged_approval_metadata(existing, payload)
errors = validate_custody_approval(data, approval_phrase)
if errors:
return data, errors
data["king_credential_ready"] = True
data["custody_mode_approved"] = True
data["custody_approved_at"] = utc_now()
data["custody_approved_by"] = approver or data.get("setup_operator", "")
data["approval_scope"] = "Non-secret local custody-mode approval only. Does not run OpenBao init."
return data, []
def print_approve_custody_mode(args: argparse.Namespace, data: dict[str, Any]) -> int:
if args.metadata is None:
print("ERROR: approve-custody-mode requires --metadata /path/to/non-secret.json", file=sys.stderr)
return 2
approval_phrase = args.approval_phrase or ""
if args.yes:
approval_phrase = APPROVAL_PHRASE
elif not approval_phrase:
print("This writes non-secret custody approval metadata only.")
print("It will not run OpenBao init and will not store secret values.")
try:
approval_phrase = input(f'Type "{APPROVAL_PHRASE}" to continue: ')
except EOFError:
approval_phrase = ""
payload: dict[str, Any] = {
"custody_mode": args.mode,
}
for key in (
"credential_label",
"identity_account_home",
"identity_account_reference",
"identity_group_reference",
"setup_operator",
"notification_contact",
"mfa_class",
"mfa_enrollment_source",
"mfa_enrollment_reference",
"notes",
):
value = getattr(args, key)
if value is not None:
payload[key] = value
if args.storage_class:
payload["storage_classes"] = args.storage_class
for field in (
"recovery_confirmed",
"custody_packet_prepared",
"no_secret_capture_confirmed",
"mfa_enrolled_confirmed",
"identity_account_created",
"identity_group_confirmed",
"oidc_login_verified",
"password_safe_confirmed",
):
if getattr(args, field):
payload[field] = True
approved, errors = approve_custody_metadata(
data,
payload,
approval_phrase,
args.approved_by or "",
)
if errors:
print("CUSTODY MODE NOT APPROVED")
print("")
for error in errors:
print(f"- {error}")
return 1
write_metadata(args.metadata, approved)
print("CUSTODY MODE APPROVED")
print("")
print(f"Metadata: {args.metadata}")
print(f"Mode: {approved['custody_mode']}")
print(f"Approved by: {approved.get('custody_approved_by', '')}")
print(f"Approved at: {approved.get('custody_approved_at', '')}")
print("")
print("OpenBao init remains a separate human-attended ceremony.")
return 0
def print_custody_packet() -> None:
print("CUSTODY PACKET TEMPLATE")
print("")
print("Credential label:")
print("Date:")
print("Setup operator/contact:")
print("Custody mode:")
print("Notification contact:")
print("")
print("Storage location description:")
print("Second-factor location description:")
print("Recovery material location description:")
print("")
print("OpenBao share assignment rows:")
print("- Share A:")
print("- Share B:")
print("- Share C:")
print("")
print("Root-token disposition:")
print("Signature/date:")
print("")
print("Do not write this packet into Git, State Hub, chat, tickets, or email.")
def print_handover_checklist() -> None:
print("HANDOVER CHECKLIST")
print("")
rows = [
"King credential kit complete.",
"OpenBao initialized and unsealed under approved custody mode.",
"Root token revoked or sealed offline.",
"Non-root platform admin path verified.",
"Bootstrap-era database credentials rotated.",
"Temporary admin accounts reviewed and removed or scoped.",
"Kubernetes service accounts and privileged bindings reviewed.",
"SOPS/age recipients and emergency bundle reviewed.",
"Backup snapshot exists.",
"Restore drill passed.",
"Audit handling known.",
"Remaining risk exceptions recorded with owner and date.",
]
for row in rows:
print(f"- {row}")
def metadata_template() -> dict[str, Any]:
return {
"bootstrap_mode": "custody",
"custodian_age_public_key": "",
"custodian_age_public_key_confirmed": False,
"custodian_age_private_key_reference": "",
"custodian_age_private_key_confirmed": False,
"credential_label": "platform-root",
"identity_account_home": "lldap",
"identity_account_reference": "",
"identity_account_created": False,
"identity_group_reference": "net-kingdom-admins",
"identity_group_confirmed": False,
"setup_operator": "tegwick",
"notification_contact": "bernd.worsch@gmail.com",
"role_setup_operator_email": "bernd.worsch@gmail.com",
"role_platform_custodian_email": "bernd.worsch@gmail.com",
"role_identity_admin_email": "bernd.worsch@gmail.com",
"role_openbao_operator_email": "bernd.worsch@gmail.com",
"role_recovery_custodian_email": "bernd.worsch@gmail.com",
"role_future_quorum_email": "",
"storage_classes": ["password-safe", "offline-packet"],
"password_safe_confirmed": False,
"mfa_class": "totp",
"mfa_enrolled_confirmed": False,
"mfa_enrollment_source": "deferred",
"mfa_enrollment_reference": "",
"recovery_confirmed": False,
"custody_packet_prepared": False,
"no_secret_capture_confirmed": False,
"king_credential_ready": False,
"custody_mode": "",
"custody_mode_approved": False,
"custody_approved_at": "",
"custody_approved_by": "",
"approval_scope": "",
"oidc_login_verified": False,
"metadata_updated_at": "",
"progress_scope": "",
"openbao_preflight_passed": False,
"openbao_init_output_produced": False,
"openbao_initialized": False,
"openbao_trial_material_exposed": False,
"openbao_compromise_response_complete": False,
"openbao_unseal_keys_rotated": False,
"root_token_disposition": "",
"restore_drill_passed": False,
"cleanup_complete": False,
"platform_reopened": False,
"review_date": "",
"notes": "Non-secret metadata only.",
}
def print_openbao_preflight(args: argparse.Namespace) -> int:
print("OPENBAO PREFLIGHT")
print("")
print("Safe commands:")
print(f"make -C {args.railiance_path} openbao-status")
print(f"make -C {args.railiance_path} openbao-verify")
print("")
if not args.run:
print("Dry run only. Pass --run to execute safe preflight commands.")
return 0
railiance_path = Path(args.railiance_path).expanduser().resolve()
if not railiance_path.is_dir():
print(f"ERROR: Railiance path not found: {railiance_path}", file=sys.stderr)
return 2
for target in ("openbao-status", "openbao-verify"):
result = subprocess.run(
["make", "-C", str(railiance_path), target],
check=False,
)
if result.returncode != 0:
return result.returncode
return 0
def gate_payload(gate: Gate) -> dict[str, str]:
return {
"name": gate.name,
"status": gate.status,
"reason": gate.reason,
}
def role_email(data: dict[str, Any], role_key: str) -> str:
fallback = str(data.get("notification_contact") or "").strip()
return str(data.get(role_key) or fallback).strip()
def role_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
rows = [
(
"setup-operator",
"Runs bootstrap commands and records non-secret evidence.",
"NetKingdom bootstrap",
"role_setup_operator_email",
),
(
"platform-root-custodian",
"Holds the dedicated platform-root credential and approves custody gates.",
"NetKingdom identity",
"role_platform_custodian_email",
),
(
"identity-admin",
"Administers LLDAP, privacyIDEA, and login repair during bootstrap.",
"Identity stack",
"role_identity_admin_email",
),
(
"openbao-ceremony-operator",
"Runs the attended OpenBao ceremony without copying secret output into the control surface.",
"Railiance OpenBao",
"role_openbao_operator_email",
),
(
"recovery-custodian",
"Can recover the platform-root credential and encrypted bootstrap bundle outside this UI.",
"Custody packet",
"role_recovery_custodian_email",
),
(
"future-quorum-custodian",
"Reserved for later two-of-three custody migration.",
"Custody strategy",
"role_future_quorum_email",
),
]
payloads: list[dict[str, str]] = []
for name, description, subsystem, key in rows:
email = role_email(data, key)
payloads.append(
{
"name": name,
"description": description,
"subsystem": subsystem,
"responsibility": name,
"email": email,
"location": "Role assignment in local bootstrap metadata." if email else "Not assigned yet.",
"state": "set" if email else "nil",
}
)
return payloads
def state_value(ok: bool, set_value: bool = False, err: bool = False) -> str:
if err:
return "err"
if ok:
return "ok"
if set_value:
return "set"
return "nil"
def subsystem_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()
return [
{
"name": "age recipient",
"description": "Public age recipient used to encrypt bootstrap bundles.",
"subsystem": "custodian age envelope",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": age_public_key_fingerprint(public_key) or "No public recipient registered.",
"state": state_value(
bool(public_key) and yes(data, "custodian_age_public_key_confirmed"),
bool(public_key),
),
},
{
"name": "platform-root user",
"description": "Dedicated LLDAP account used as the current king credential.",
"subsystem": "LLDAP",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": str(data.get("identity_account_reference") or "Not recorded."),
"state": state_value(identity_account_ready(data), bool(data.get("identity_account_reference"))),
},
{
"name": "platform-root MFA token",
"description": "Second factor enrolled with the authority that verifies login.",
"subsystem": "privacyIDEA",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": str(data.get("mfa_enrollment_reference") or "Not recorded."),
"state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")),
},
{
"name": "bootstrap OIDC client",
"description": "KeyCape login path used to verify platform-root can authenticate with MFA.",
"subsystem": "KeyCape",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": KEYCAPE_ISSUER,
"state": state_value(identity_login_ready(data), bool(data.get("identity_account_reference"))),
},
{
"name": "openbao-0",
"description": "Railiance OpenBao pod, services, PVCs, and sealed pre-init state.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "namespace=openbao, pod=openbao-0",
"state": state_value(yes(data, "openbao_preflight_passed"), yes(data, "custody_mode_approved")),
},
{
"name": "encrypted bootstrap bundle",
"description": "Encrypted bootstrap secret bundle; plaintext directory must be absent.",
"subsystem": "sso-mfa/bootstrap",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": str(state["encrypted_bundle_path"]),
"state": state_value(
bool(state["encrypted_bundle_exists"]) and not bool(state["plaintext_secrets_present"]),
bool(state["encrypted_bundle_exists"]),
bool(state["plaintext_secrets_present"]),
),
},
]
def integration_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
return [
{
"name": "LLDAP admin group assignment",
"description": "platform-root is assigned to the current NetKingdom admin group.",
"subsystem": "LLDAP",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": str(data.get("identity_group_reference") or "Not recorded."),
"state": state_value(yes(data, "identity_group_confirmed"), bool(data.get("identity_group_reference"))),
},
{
"name": "privacyIDEA MFA verification",
"description": "The same platform-root account has an enrolled second factor.",
"subsystem": "privacyIDEA",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": str(data.get("mfa_enrollment_reference") or "Not recorded."),
"state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")),
},
{
"name": "KeyCape OIDC login",
"description": "platform-root completed the OIDC login check through KeyCape.",
"subsystem": "KeyCape",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": "local bootstrap callback",
"state": state_value(yes(data, "oidc_login_verified")),
},
{
"name": "OpenBao preflight",
"description": "Railiance status and verify targets passed in the approved pre-init state.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "../railiance-platform",
"state": state_value(yes(data, "openbao_preflight_passed"), yes(data, "custody_mode_approved")),
},
{
"name": "OpenBao init/unseal ceremony",
"description": "Attended ceremony creates unseal shares and initial root token outside this UI.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "operator shell with custody packet present",
"state": state_value(yes(data, "openbao_initialized"), yes(data, "openbao_preflight_passed")),
},
]
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()
root_disposition = str(data.get("root_token_disposition") or "")
init_output = yes(data, "openbao_init_output_produced")
return [
{
"name": "platform-root",
"description": "Dedicated LLDAP user for the king credential.",
"subsystem": "LLDAP",
"responsibility": "platform-root-custodian",
"email": role_email(data, "role_platform_custodian_email"),
"location": str(data.get("identity_account_reference") or "Not recorded."),
"state": state_value(identity_account_ready(data), bool(data.get("identity_account_reference"))),
},
{
"name": "net-kingdom-admins",
"description": "Current lightweight admin group for the platform-root identity.",
"subsystem": "LLDAP",
"responsibility": "identity-admin",
"email": role_email(data, "role_identity_admin_email"),
"location": str(data.get("identity_group_reference") or "Not recorded."),
"state": state_value(yes(data, "identity_group_confirmed"), bool(data.get("identity_group_reference"))),
},
{
"name": "platform-root password entry",
"description": "Password-safe entry for the dedicated identity password. Value is never stored here.",
"subsystem": "password safe",
"responsibility": "platform-root-custodian",
"email": role_email(data, "role_platform_custodian_email"),
"location": "operator password safe / offline packet",
"state": state_value(yes(data, "password_safe_confirmed")),
},
{
"name": "TOTP token",
"description": "privacyIDEA token for the platform-root login path.",
"subsystem": "privacyIDEA",
"responsibility": "platform-root-custodian",
"email": role_email(data, "role_platform_custodian_email"),
"location": str(data.get("mfa_enrollment_reference") or "Not recorded."),
"state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")),
},
{
"name": "age recipient",
"description": "Public recipient used for encrypted bootstrap bundles.",
"subsystem": "custodian age envelope",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": age_public_key_fingerprint(public_key) or "Not recorded.",
"state": state_value(bool(public_key) and yes(data, "custodian_age_public_key_confirmed"), bool(public_key)),
},
{
"name": "age private key reference",
"description": "Non-secret pointer to the private age key location.",
"subsystem": "custodian age envelope",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": str(data.get("custodian_age_private_key_reference") or "Not recorded."),
"state": state_value(yes(data, "custodian_age_private_key_confirmed"), bool(data.get("custodian_age_private_key_reference"))),
},
{
"name": "secrets.enc",
"description": "Encrypted bootstrap bundle.",
"subsystem": "sso-mfa/bootstrap",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": str(state["encrypted_bundle_path"]),
"state": state_value(bool(state["encrypted_bundle_exists"]), False, bool(state["plaintext_secrets_present"])),
},
{
"name": "custody strategy",
"description": "Selected OpenBao ceremony control model.",
"subsystem": "custody model",
"responsibility": "platform-root-custodian",
"email": role_email(data, "role_platform_custodian_email"),
"location": str(data.get("custody_mode") or "Not selected."),
"state": state_value(yes(data, "custody_mode_approved"), data.get("custody_mode") in VALID_CUSTODY_MODES),
},
{
"name": "recovery material",
"description": "Recovery references for identity, MFA, age key, and encrypted bootstrap bundle.",
"subsystem": "custody packet",
"responsibility": "recovery-custodian",
"email": role_email(data, "role_recovery_custodian_email"),
"location": "offline packet / password safe references",
"state": state_value(yes(data, "recovery_confirmed")),
},
{
"name": "OpenBao custody packet",
"description": "Ceremony envelope with share assignment slots and root-token disposition plan.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "offline ceremony packet",
"state": state_value(yes(data, "custody_packet_prepared")),
},
{
"name": "unseal shares",
"description": "Real OpenBao shares produced by init. They must be routed directly to approved custody locations.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "created during attended init; not stored here",
"state": state_value(yes(data, "openbao_unseal_keys_rotated"), init_output or yes(data, "openbao_initialized")),
},
{
"name": "initial root token",
"description": "OpenBao bootstrap token produced by init. Use only for first configuration and disposition.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": "created during attended init; never pasted here",
"state": state_value(root_disposition in {"revoked", "offline-sealed"}, init_output or yes(data, "openbao_initialized")),
},
]
def command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
preflight_done = yes(data, "openbao_preflight_passed")
custody_approved = custody_mode_approved(data)
init_output = yes(data, "openbao_init_output_produced")
initialized = yes(data, "openbao_initialized")
trial_exposed = yes(data, "openbao_trial_material_exposed")
keys_rotated = yes(data, "openbao_unseal_keys_rotated")
root_disposed = data.get("root_token_disposition") in {"revoked", "offline-sealed"}
restore_done = yes(data, "restore_drill_passed")
status_state = "todo"
status_reason = "Run any time to inspect the current OpenBao deployment state."
if preflight_done:
status_state = "done"
status_reason = "Deployment and pre-init status were verified."
if (init_output or initialized) and not root_disposed:
status_state = "redo"
status_reason = "OpenBao changed during init/unseal; rerun status before root-token disposition."
preflight_state = "done" if preflight_done else "todo"
preflight_reason = "Safe preflight passed."
if not preflight_done:
preflight_reason = "Run after custody approval and before init."
if not custody_approved:
preflight_state = "blocked"
preflight_reason = "Approve the selected custody strategy first."
init_state = "done" if init_output or initialized else "todo"
init_reason = "Init output was produced. Do not paste unseal shares or root token here."
if not (init_output or initialized):
init_reason = "Run once, attended, after OpenBao preflight."
if not preflight_done:
init_state = "blocked"
init_reason = "OpenBao preflight must pass first."
unseal_state = "done" if initialized else "todo"
unseal_reason = "OpenBao is recorded as initialized and unsealed."
if not initialized:
unseal_reason = "Provide threshold shares by prompt, not as command arguments."
if not (init_output or initialized):
unseal_state = "blocked"
unseal_reason = "OpenBao init output must be produced first."
if trial_exposed and initialized and not keys_rotated:
config_state = "blocked"
config_reason = "Trial key material is exposed; rotate unseal keys or reset before configuration."
else:
config_state = "done" if root_disposed else "todo"
config_reason = "Initial configuration and root-token disposition are recorded."
if not root_disposed:
config_reason = "Configure OpenBao, then revoke or offline-seal the root token."
if not initialized:
config_state = "blocked"
config_reason = "OpenBao must be initialized and unsealed first."
verify_state = "done" if restore_done else "todo"
verify_reason = "Restore proof has been recorded."
if not restore_done:
verify_reason = "Verify post-unseal readiness, snapshot, and isolated restore."
if not initialized:
verify_state = "blocked"
verify_reason = "OpenBao must be initialized and unsealed first."
return [
{
"name": "OpenBao status",
"description": "Show pod, service, PVC, and seal/init status.",
"status": status_state,
"status_reason": status_reason,
"command": "make -C ../railiance-platform openbao-status",
},
{
"name": "OpenBao preflight",
"description": "Run safe status and verification checks. Does not initialize OpenBao.",
"status": preflight_state,
"status_reason": preflight_reason,
"command": (
"python3 tools/security-bootstrap-console/security_bootstrap_console.py "
"openbao-preflight --railiance-path ../railiance-platform --run"
),
},
{
"name": "OpenBao init ceremony",
"description": "Creates real unseal shares and the initial root token. Run once, attended.",
"status": init_state,
"status_reason": init_reason,
"command": "kubectl exec -n openbao openbao-0 -- bao operator init -key-shares=3 -key-threshold=2",
},
{
"name": "OpenBao unseal prompt",
"description": "Enter unseal shares by interactive terminal prompt. Do not place shares on the command line.",
"status": unseal_state,
"status_reason": unseal_reason,
"command": "kubectl exec -it -n openbao openbao-0 -- bao operator unseal",
},
{
"name": "OpenBao initial configuration",
"description": "Apply first audit, auth, mount, and policy configuration after unseal.",
"status": config_state,
"status_reason": config_reason,
"command": "make -C ../railiance-platform openbao-configure-initial",
},
{
"name": "OpenBao post-unseal verification",
"description": "Verify filesystem and post-unseal readiness before live secrets move in.",
"status": verify_state,
"status_reason": verify_reason,
"command": "make -C ../railiance-platform openbao-verify-post-unseal",
},
]
def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
init_output = yes(data, "openbao_init_output_produced")
initialized = yes(data, "openbao_initialized")
trial_exposed = yes(data, "openbao_trial_material_exposed")
response_complete = yes(data, "openbao_compromise_response_complete")
keys_rotated = yes(data, "openbao_unseal_keys_rotated")
key_compromise_status = "done" if response_complete else "todo"
key_compromise_location = "Use for trial output exposure, screenshots, chat paste, shell history, or lost custody."
if not trial_exposed:
key_compromise_status = "blocked" if not init_output else "todo"
key_compromise_location = "Mark trial key material exposed before running the response checklist."
rotate_status = "done" if keys_rotated else "todo"
rotate_location = "Run only after OpenBao is unsealed and existing exposed shares are available for quorum."
if not initialized:
rotate_status = "blocked"
rotate_location = "Unseal OpenBao first; rotate-keys needs a quorum of current unseal shares."
if not trial_exposed and not keys_rotated:
rotate_status = "blocked"
rotate_location = "Record the key-compromise condition or schedule a normal rotation first."
return [
{
"name": "Key material compromised",
"description": "Respond when init output, unseal shares, or root-token material escaped the custody boundary.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": key_compromise_location,
"state": key_compromise_status,
},
{
"name": "Generate new unseal keys",
"description": "Rotate OpenBao Shamir unseal shares after a trial exposure or planned custody migration.",
"subsystem": "Railiance OpenBao",
"responsibility": "openbao-ceremony-operator",
"email": role_email(data, "role_openbao_operator_email"),
"location": rotate_location,
"state": rotate_status,
},
]
def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
init_output = yes(data, "openbao_init_output_produced")
initialized = yes(data, "openbao_initialized")
trial_exposed = yes(data, "openbao_trial_material_exposed")
response_complete = yes(data, "openbao_compromise_response_complete")
keys_rotated = yes(data, "openbao_unseal_keys_rotated")
exposure_status = "done" if trial_exposed else "todo"
exposure_reason = "Trial key-material exposure is recorded in non-secret metadata." if trial_exposed else "Record that the trial init output escaped custody before using affected material."
response_status = "done" if response_complete else "todo"
response_reason = "Compromise response was recorded." if response_complete else "Stop production use of exposed material, decide rotate-vs-reset, and record non-secret evidence."
if not trial_exposed:
response_status = "blocked"
response_reason = "Record the key-material exposure first."
unseal_status = "done" if initialized else "todo"
unseal_reason = "OpenBao is unsealed." if initialized else "Unseal by hidden prompt before rotating unseal keys."
if not init_output:
unseal_status = "blocked"
unseal_reason = "OpenBao init output must exist first."
rotate_status = "done" if keys_rotated else "todo"
rotate_reason = "New unseal keys are recorded as generated." if keys_rotated else "Start rotation, then submit current shares by prompt until quorum completes."
if not initialized:
rotate_status = "blocked"
rotate_reason = "OpenBao must be unsealed before rotate-keys can run."
if not trial_exposed and not keys_rotated:
rotate_status = "blocked"
rotate_reason = "Record exposure or schedule a normal rotation before generating new shares."
return [
{
"name": "Record key exposure",
"description": "Non-secret metadata checkbox in this UI; do not paste exposed values.",
"status": exposure_status,
"status_reason": exposure_reason,
"command": "Use the checkbox: Trial key material exposed",
},
{
"name": "Unseal by prompt",
"description": "Provide threshold shares interactively. Never put shares on the command line.",
"status": unseal_status,
"status_reason": unseal_reason,
"command": "kubectl exec -it -n openbao openbao-0 -- bao operator unseal",
},
{
"name": "Start unseal-key rotation",
"description": "Generate a new 3-share, threshold-2 Shamir split after compromise or planned migration.",
"status": rotate_status,
"status_reason": rotate_reason,
"command": "kubectl exec -it -n openbao openbao-0 -- bao operator rotate-keys -init -key-shares=3 -key-threshold=2",
},
{
"name": "Submit current shares for rotation",
"description": "Repeat by prompt until the required threshold completes. Use the nonce from rotation init.",
"status": rotate_status,
"status_reason": rotate_reason,
"command": "kubectl exec -it -n openbao openbao-0 -- bao operator rotate-keys -nonce=<nonce-from-rotation-init>",
},
{
"name": "Cancel key rotation",
"description": "Abort a started rotation if the nonce, share handling, or ceremony context is wrong.",
"status": "todo" if initialized and not keys_rotated else "blocked",
"status_reason": "Available while a rotation is in progress." if initialized and not keys_rotated else "No active rotation expected.",
"command": "kubectl exec -it -n openbao openbao-0 -- bao operator rotate-keys -cancel",
},
{
"name": "Record compromise response complete",
"description": "Non-secret metadata checkbox after exposed material is rotated or the trial environment is reset.",
"status": response_status,
"status_reason": response_reason,
"command": "Use the checkbox: Compromise response complete",
},
]
def section_gate_payloads(data: dict[str, Any]) -> list[dict[str, str]]:
role_rows = role_payloads(data)
role_ok = all(row["state"] != "nil" for row in role_rows[:5])
subsystem_rows = subsystem_payloads(data)
integration_rows = integration_payloads(data)
runbook_rows = runbook_payloads(data)
artifact_rows = artifact_payloads(data)
return [
{
"key": "roles",
"name": "Roles & Responsibilities",
"status": "ok" if role_ok else "set",
"reason": "All active bootstrap roles have a designated email." if role_ok else "Assign an email to each active bootstrap role.",
},
{
"key": "subsystems",
"name": "Subsystems & Scope",
"status": "ok" if all(row["state"] in {"ok", "set"} for row in subsystem_rows) else "err",
"reason": "Subsystems have install/access evidence." if all(row["state"] != "nil" for row in subsystem_rows) else "Complete subsystem setup fields and confirmations.",
},
{
"key": "integrations",
"name": "Integration & Tests",
"status": "ok" if all(row["state"] == "ok" for row in integration_rows[:4]) else "set",
"reason": "Identity and OpenBao preflight checks are done." if all(row["state"] == "ok" for row in integration_rows[:4]) else "Run or confirm the remaining integration checks.",
},
{
"key": "runbooks",
"name": "Usecases & Runbooks",
"status": "ok" if all(row["state"] in {"done", "blocked"} for row in runbook_rows) else "set",
"reason": "Runbook states are recorded." if all(row["state"] in {"done", "blocked"} for row in runbook_rows) else "Review active runbooks and record non-secret outcomes.",
},
{
"key": "artifacts",
"name": "Artefacts & Locations",
"status": "ok" if all(row["state"] != "nil" for row in artifact_rows[:10]) else "set",
"reason": "Core custody artefacts have locations or confirmations." if all(row["state"] != "nil" for row in artifact_rows[:10]) else "Record missing artefact locations and confirmations.",
},
]
def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
merged = metadata_template()
merged.update(data)
gates = build_gates(merged)
metadata_view = dict(merged)
public_key = extract_age_public_key(metadata_view.get("custodian_age_public_key"))
metadata_view["custodian_age_public_key"] = public_key
metadata_view["custodian_age_public_key_fingerprint"] = age_public_key_fingerprint(public_key)
for role_key in (
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email",
):
if not metadata_view.get(role_key):
metadata_view[role_key] = role_email(merged, role_key)
return {
"metadata_path": str(metadata_path),
"stage": derive_stage(merged),
"next_action": next_action(gates, kit_validation(merged), merged),
"gates": [gate_payload(gate) for gate in gates],
"key_custody_gates": [gate_payload(gate) for gate in key_custody_validation(merged)],
"kit_gates": [gate_payload(gate) for gate in kit_validation(merged)],
"section_gates": section_gate_payloads(merged),
"roles": role_payloads(merged),
"subsystems": subsystem_payloads(merged),
"integrations": integration_payloads(merged),
"runbooks": runbook_payloads(merged),
"artifacts": artifact_payloads(merged),
"commands": command_payloads(merged),
"runbook_commands": runbook_command_payloads(merged),
"bootstrap_secret_state": bootstrap_secret_state(),
"metadata": metadata_view,
"approval_phrase": APPROVAL_PHRASE,
"custody_approval_modes": sorted(CUSTODY_APPROVAL_MODES),
}
def oidc_code_challenge() -> str:
digest = hashlib.sha256(OIDC_CODE_VERIFIER.encode("ascii")).digest()
return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
def local_oidc_redirect_uri(host: str) -> str:
clean_host = host.strip() or "127.0.0.1:8876"
return f"http://{clean_host}/oidc/callback"
def local_oidc_start_url(host: str) -> str:
params = {
"response_type": "code",
"client_id": OIDC_CLIENT_ID,
"redirect_uri": local_oidc_redirect_uri(host),
"scope": OIDC_SCOPE,
"state": "netkingdom-bootstrap-login-check",
"code_challenge": oidc_code_challenge(),
"code_challenge_method": "S256",
}
return f"{KEYCAPE_ISSUER}/authorize?{urllib.parse.urlencode(params)}"
def decode_jwt_payload(token: str) -> dict[str, Any]:
parts = token.split(".")
if len(parts) < 2:
return {}
payload = parts[1]
payload += "=" * (-len(payload) % 4)
try:
decoded = base64.urlsafe_b64decode(payload.encode("ascii"))
claims = json.loads(decoded)
except (ValueError, json.JSONDecodeError):
return {}
return claims if isinstance(claims, dict) else {}
def exchange_oidc_code(code: str, host: str) -> dict[str, Any]:
form = urllib.parse.urlencode(
{
"grant_type": "authorization_code",
"client_id": OIDC_CLIENT_ID,
"code": code,
"code_verifier": OIDC_CODE_VERIFIER,
"redirect_uri": local_oidc_redirect_uri(host),
}
).encode("utf-8")
request = urllib.request.Request(
f"{KEYCAPE_ISSUER}/token",
data=form,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(request, timeout=10) as response:
payload = json.loads(response.read().decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("token endpoint returned a non-object JSON payload")
return payload
def oidc_result_html(query: str, host: str) -> str:
params = urllib.parse.parse_qs(query)
error = params.get("error", [""])[0]
description = params.get("error_description", [""])[0]
code = params.get("code", [""])[0]
state = params.get("state", [""])[0]
title = "OIDC Login Check"
status = "Waiting for callback result."
rows: list[tuple[str, str]] = []
note = (
"No tokens or OTP values are stored by this local page. If token exchange "
"succeeds, only non-secret claims are shown."
)
if error:
status = "Login did not complete."
rows.append(("Error", error))
if description:
rows.append(("Description", description))
elif not code:
status = "No authorization code was returned."
note = (
"Start the check from the bootstrap console. If the browser never "
"returns here, KeyCape may still need its public Authelia redirect "
"configuration or a browser OTP prompt."
)
else:
try:
token_payload = exchange_oidc_code(code, host)
claims = decode_jwt_payload(str(token_payload.get("access_token", "")))
status = "OIDC login path completed."
rows.extend(
[
("State", state or "(none)"),
("Issuer", str(claims.get("iss", ""))),
("Audience", str(claims.get("aud", ""))),
("Subject", str(claims.get("sub", ""))),
("Username", str(claims.get("preferred_username", ""))),
("Email", str(claims.get("email", ""))),
("Groups", json.dumps(claims.get("groups", []))),
]
)
note = (
"Return to the bootstrap console, check OIDC login verified for "
"the same account, and save progress."
)
except urllib.error.HTTPError as exc:
body = exc.read(1000).decode("utf-8", "replace")
status = "Authorization returned, but token exchange failed."
rows.extend(
[
("HTTP status", str(exc.code)),
("Endpoint", f"{KEYCAPE_ISSUER}/token"),
("Response", body),
]
)
note = (
"This usually means the live KeyCape config has not yet registered "
"this local callback URI, the code expired, or the OTP browser "
"prompt path is still incomplete."
)
except Exception as exc: # noqa: BLE001 - local diagnostic page
status = "Authorization returned, but token exchange could not run."
rows.append(("Error", str(exc)))
table_rows = "\n".join(
f"<tr><th>{html.escape(label)}</th><td>{html.escape(value)}</td></tr>"
for label, value in rows
)
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title>
<style>
body {{ margin: 0; background: #f5f2e9; color: #111; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; }}
main {{ width: min(760px, 100%); margin: 0 auto; padding: 28px; }}
section {{ border: 1px solid #111; border-radius: 6px; background: #fffdf7; padding: 20px; }}
h1 {{ margin: 0 0 12px; font-size: 24px; }}
p {{ line-height: 1.4; }}
table {{ width: 100%; border-collapse: collapse; margin: 16px 0; background: #fff; }}
th, td {{ border: 1px solid #d8d3c7; padding: 9px; text-align: left; vertical-align: top; overflow-wrap: anywhere; }}
th {{ width: 150px; }}
a {{ display: inline-flex; min-height: 38px; align-items: center; justify-content: center; border: 1px solid #111; border-radius: 4px; background: #111; color: #fff; padding: 8px 12px; text-decoration: none; font-weight: 650; }}
</style>
</head>
<body>
<main>
<section>
<h1>{html.escape(status)}</h1>
<p>{html.escape(note)}</p>
<table>{table_rows}</table>
<a href="/" title="Return to the local NetKingdom bootstrap console.">Return to bootstrap console</a>
</section>
</main>
</body>
</html>"""
def ui_html() -> str:
return """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NetKingdom Security Bootstrap</title>
<style>
:root {
--ink: #111111;
--muted: #555555;
--paper: #fffdf7;
--field: #ffffff;
--line: #1d1d1d;
--soft-line: #d8d3c7;
--background: #f5f2e9;
--hi: #ffe14a;
--ok: #dceee5;
--warn: #fff2b8;
--human: #e6ecf7;
--bad: #f4d6d0;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--background);
color: var(--ink);
font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
letter-spacing: 0;
}
header {
border-bottom: 1px solid var(--line);
background: var(--paper);
padding: 22px 28px;
}
.eyebrow {
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
letter-spacing: 0;
text-transform: uppercase;
}
h1 {
margin: 8px 0 0;
font-size: 28px;
line-height: 1.15;
font-weight: 650;
}
main {
width: min(1180px, 100%);
margin: 0 auto;
padding: 24px;
}
.topline {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 1px solid var(--line);
background: var(--paper);
border-radius: 6px;
}
.metric {
min-width: 0;
padding: 16px;
border-right: 1px solid var(--soft-line);
}
.metric:last-child { border-right: 0; }
.label {
display: block;
color: var(--muted);
font-size: 13px;
margin-bottom: 6px;
}
.value {
display: block;
font-size: 17px;
line-height: 1.25;
overflow-wrap: anywhere;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.72fr);
gap: 18px;
margin-top: 18px;
align-items: start;
}
.panel {
border: 1px solid var(--line);
border-radius: 6px;
background: var(--paper);
padding: 18px;
}
.panel + .panel { margin-top: 18px; }
h2 {
margin: 0 0 14px;
font-size: 18px;
line-height: 1.2;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.field { min-width: 0; }
input[type="text"], select {
width: 100%;
min-height: 42px;
border: 1px solid var(--line);
border-radius: 4px;
background: var(--field);
color: var(--ink);
font: inherit;
padding: 9px 10px;
}
input[type="checkbox"], input[type="radio"] {
accent-color: var(--ink);
width: 16px;
height: 16px;
margin: 0;
}
.choice-list {
display: grid;
gap: 8px;
}
.choice {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 10px;
align-items: start;
border: 1px solid var(--soft-line);
border-radius: 4px;
background: #ffffff;
padding: 10px;
}
.choice strong {
display: block;
font-weight: 650;
line-height: 1.2;
}
.step-number {
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border: 1px solid var(--line);
border-radius: 999px;
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
background: var(--hi);
}
.choice span {
color: var(--muted);
display: block;
font-size: 13px;
line-height: 1.3;
margin-top: 3px;
}
.notice {
border-left: 6px solid var(--hi);
background: #fff9d8;
padding: 12px 14px;
line-height: 1.35;
margin: 0 0 16px;
}
.spec-list {
margin: 0 0 16px;
padding-left: 20px;
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.spec-list li { margin: 5px 0; }
.system-note {
border: 1px solid var(--soft-line);
background: #ffffff;
padding: 12px 14px;
line-height: 1.35;
margin: 0 0 16px;
}
.conditional { display: none; }
.conditional.visible { display: grid; }
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-top: 16px;
}
.inline-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
button {
min-height: 42px;
border: 1px solid var(--ink);
border-radius: 4px;
background: var(--ink);
color: #ffffff;
font: inherit;
font-weight: 650;
padding: 10px 14px;
cursor: pointer;
}
button.secondary {
background: var(--paper);
color: var(--ink);
}
.button-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
border: 1px solid var(--ink);
border-radius: 4px;
background: var(--ink);
color: #ffffff;
font-weight: 650;
padding: 7px 11px;
text-decoration: none;
}
.button-link.secondary {
background: var(--paper);
color: var(--ink);
}
button:disabled {
cursor: wait;
opacity: 0.65;
}
summary {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
cursor: pointer;
list-style: none;
}
summary::-webkit-details-marker { display: none; }
.summary-title {
font-size: 18px;
line-height: 1.2;
font-weight: 650;
}
.section-gate {
border: 1px solid var(--soft-line);
border-radius: 4px;
background: #ffffff;
padding: 10px 12px;
margin: 14px 0;
color: var(--muted);
font-size: 13px;
line-height: 1.35;
}
.record-list, .command-list {
display: grid;
gap: 8px;
margin: 14px 0;
}
.record-row {
display: grid;
grid-template-columns: 58px minmax(170px, 1.25fr) minmax(130px, 0.72fr) minmax(150px, 1fr);
gap: 10px;
align-items: start;
border: 1px solid var(--soft-line);
border-radius: 4px;
background: #ffffff;
padding: 10px;
}
.record-name {
font-weight: 650;
line-height: 1.2;
}
.record-description, .record-meta {
color: var(--muted);
font-size: 13px;
line-height: 1.35;
margin-top: 3px;
overflow-wrap: anywhere;
}
.record-context {
display: grid;
gap: 6px;
min-width: 0;
}
.state {
display: inline-flex;
justify-content: center;
min-width: 42px;
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 8px;
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
line-height: 1.2;
text-transform: uppercase;
background: var(--warn);
}
.state.ok { background: var(--ok); }
.state.done { background: var(--ok); }
.state.set { background: var(--human); }
.state.redo { background: var(--human); }
.state.nil { background: #ffffff; }
.state.todo { background: var(--warn); }
.state.err { background: var(--bad); }
.state.blocked { background: var(--bad); }
.role-chip {
display: inline-flex;
max-width: 100%;
border: 1px solid var(--soft-line);
border-radius: 999px;
padding: 3px 8px;
background: #ffffff;
color: var(--ink);
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
overflow-wrap: anywhere;
}
.command-row {
border: 1px solid var(--soft-line);
border-radius: 4px;
background: #ffffff;
padding: 10px;
}
.command-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: start;
}
.command-code {
display: block;
width: 100%;
margin-top: 8px;
border: 1px solid var(--line);
border-radius: 4px;
background: #181818;
color: #ffffff;
padding: 10px 12px;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.copy-button {
min-height: 32px;
padding: 6px 10px;
font-size: 13px;
}
.gates {
border: 1px solid var(--soft-line);
border-radius: 4px;
overflow: hidden;
}
.gate {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
gap: 12px;
padding: 11px 12px;
border-top: 1px solid var(--soft-line);
background: #ffffff;
}
.gate:first-child { border-top: 0; }
.pill {
align-self: start;
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 8px;
text-align: center;
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 12px;
line-height: 1.2;
text-transform: uppercase;
background: var(--warn);
}
.done .pill { background: var(--ok); }
.human .pill { background: var(--human); }
.blocked .pill { background: var(--bad); }
.gate-title {
font-weight: 650;
line-height: 1.2;
}
.gate-reason {
color: var(--muted);
font-size: 13px;
line-height: 1.35;
margin-top: 3px;
}
.message {
margin-top: 12px;
border: 1px solid var(--line);
border-radius: 4px;
background: #ffffff;
padding: 12px;
min-height: 46px;
white-space: pre-line;
}
.message.ok { background: var(--ok); }
.message.error { background: var(--bad); }
code {
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 13px;
}
a {
color: var(--ink);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
@media (max-width: 820px) {
header { padding: 18px; }
main { padding: 16px; }
.topline, .layout, .grid { grid-template-columns: 1fr; }
.record-row { grid-template-columns: 1fr; }
.metric {
border-right: 0;
border-bottom: 1px solid var(--soft-line);
}
.metric:last-child { border-bottom: 0; }
}
</style>
</head>
<body>
<header>
<div class="eyebrow">NetKingdom control surface</div>
<h1>Guided security bootstrap</h1>
</header>
<main>
<section class="topline" aria-label="Bootstrap status">
<div class="metric">
<span class="label">Stage</span>
<span class="value" id="stage">Loading</span>
</div>
<div class="metric">
<span class="label">Next safe action</span>
<span class="value" id="next-action">Loading</span>
</div>
<div class="metric">
<span class="label">Metadata</span>
<span class="value"><code id="metadata-path">Loading</code></span>
</div>
</section>
<div class="layout">
<form id="approval-form">
<details class="panel workflow-section" data-section="roles" open>
<summary><span class="summary-title">1. Roles & Responsibilities</span><span class="state nil" data-section-state="roles">nil</span></summary>
<div class="section-gate" data-section-gate="roles">Loading role gate.</div>
<p class="notice">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.</p>
<div id="roles-records" class="record-list"></div>
<details id="responsibility-editor" class="system-note">
<summary><span class="record-name">Change responsibilities</span><span class="state set" id="responsibility-edit-state">set</span></summary>
<div class="grid" style="margin-top: 14px;">
<label class="field">
<span class="label">Setup operator</span>
<input id="setup_operator" type="text" autocomplete="off" title="Local operator handle used for non-secret progress records.">
</label>
<label class="field">
<span class="label">Notification contact</span>
<input id="notification_contact" type="text" autocomplete="off" title="Email for bootstrap notifications and lockout handling.">
</label>
<label class="field">
<span class="label">Setup operator email</span>
<input id="role_setup_operator_email" type="text" autocomplete="off" title="Designated user for the setup-operator role.">
</label>
<label class="field">
<span class="label">Platform-root custodian email</span>
<input id="role_platform_custodian_email" type="text" autocomplete="off" title="Designated user for the platform-root-custodian role.">
</label>
<label class="field">
<span class="label">Identity admin email</span>
<input id="role_identity_admin_email" type="text" autocomplete="off" title="Designated user for identity-admin during bootstrap.">
</label>
<label class="field">
<span class="label">OpenBao operator email</span>
<input id="role_openbao_operator_email" type="text" autocomplete="off" title="Designated user for the attended OpenBao ceremony.">
</label>
<label class="field">
<span class="label">Recovery custodian email</span>
<input id="role_recovery_custodian_email" type="text" autocomplete="off" title="Designated user for recovery material and bootstrap bundle custody.">
</label>
<label class="field">
<span class="label">Future quorum email</span>
<input id="role_future_quorum_email" type="text" autocomplete="off" title="Optional placeholder for later two-of-three custody migration.">
</label>
</div>
<div class="actions">
<button class="secondary" id="responsibility-cancel-button" type="button" disabled title="Discard responsibility edits and close this foldout.">Cancel</button>
<button id="responsibility-save-button" type="button" disabled title="Save responsibility edits as non-secret local metadata and close this foldout.">Save</button>
</div>
</details>
</details>
<details class="panel workflow-section" data-section="subsystems" open>
<summary><span class="summary-title">2. Subsystems & Scope</span><span class="state nil" data-section-state="subsystems">nil</span></summary>
<div class="section-gate" data-section-gate="subsystems">Loading subsystem gate.</div>
<p class="notice">This section is about installing each subsystem and establishing initial user access. Integration checks come later.</p>
<div id="subsystems-records" class="record-list"></div>
<div class="grid">
<label class="field">
<span class="label">Bootstrap mode</span>
<select id="bootstrap_mode" title="Trial mode documents the process; custody mode is for real handover material.">
<option value="trial">Trial</option>
<option value="custody">Custody</option>
</select>
</label>
<label class="field">
<span class="label">Credential label</span>
<input id="credential_label" type="text" autocomplete="off" title="Dedicated credential label, usually platform-root.">
</label>
<label class="field">
<span class="label">Custodian public age key</span>
<input id="custodian_age_public_key" type="text" autocomplete="off" placeholder="age1..." title="Public recipient only. Never paste AGE-SECRET-KEY material here.">
</label>
<label class="field">
<span class="label">Public key fingerprint</span>
<input id="custodian_age_public_key_fingerprint" type="text" readonly title="Derived from the public age recipient.">
</label>
<label class="field">
<span class="label">Private key custody reference</span>
<input id="custodian_age_private_key_reference" type="text" autocomplete="off" placeholder="KeePassXC: custodian/age/private" title="Non-secret pointer to where the private key is held; not the key itself.">
</label>
<label class="field">
<span class="label">Account home</span>
<input id="identity_account_home" type="text" autocomplete="off" value="lldap" title="Identity subsystem where the platform-root account lives.">
</label>
<label class="field">
<span class="label">Account reference</span>
<input id="identity_account_reference" type="text" autocomplete="off" placeholder="platform-root@lldap" title="Non-secret account reference.">
</label>
<label class="field">
<span class="label">Admin group</span>
<input id="identity_group_reference" type="text" autocomplete="off" value="net-kingdom-admins" title="LLDAP group used for current bootstrap admin access.">
</label>
<label class="field">
<span class="label">Second factor</span>
<select id="mfa_class" title="Hardware token is optional policy, not a default requirement.">
<option value="">Select</option>
<option value="totp">TOTP</option>
<option value="webauthn">WebAuthn</option>
<option value="hardware-token">Hardware token (policy only)</option>
</select>
</label>
<label class="field">
<span class="label">MFA enrollment source</span>
<select id="mfa_enrollment_source" title="The real verifier owns the QR code or setup key.">
<option value="deferred">Deferred</option>
<option value="identity-provider">Identity provider</option>
<option value="external-verifier">External verifier</option>
<option value="hardware-registration">Hardware registration</option>
</select>
</label>
<label class="field">
<span class="label">MFA reference</span>
<input id="mfa_enrollment_reference" type="text" autocomplete="off" placeholder="provider or vault entry label" title="Non-secret reference to the enrollment authority or vault label.">
</label>
</div>
<div class="choice-list" style="margin-top: 14px;">
<label class="choice"><input id="custodian_age_public_key_confirmed" type="checkbox"><span><strong>Public key confirmed</strong><span>The public recipient matches the custodian key material you intend to use.</span></span></label>
<label class="choice"><input id="custodian_age_private_key_confirmed" type="checkbox"><span><strong>Private key location confirmed</strong><span>You know where the private key is stored and can unlock it intentionally later.</span></span></label>
<label class="choice"><input id="identity_account_created" type="checkbox"><span><strong>Account created</strong><span>The dedicated identity account exists in LLDAP. No password is stored here.</span></span></label>
<label class="choice"><input id="identity_group_confirmed" type="checkbox"><span><strong>Admin group assigned</strong><span>The account is a member of <code>net-kingdom-admins</code> in LLDAP.</span></span></label>
<label class="choice"><input id="password_safe_confirmed" type="checkbox"><span><strong>Password stored</strong><span>The account password is stored in your password safe or offline custody packet.</span></span></label>
<label class="choice"><input id="mfa_enrolled_confirmed" type="checkbox"><span><strong>Factor enrolled</strong><span>The real verifier produced or accepted the factor. No seed is recorded here.</span></span></label>
</div>
<div class="inline-actions">
<a class="button-link" href="https://lldap.coulomb.social" target="_blank" rel="noreferrer" title="Open the LLDAP admin UI. This path uses password auth only and must be restricted before production.">Open LLDAP</a>
<a class="button-link secondary" href="https://pink-account.coulomb.social" target="_blank" rel="noreferrer" title="Open privacyIDEA self-service to enroll or test the platform-root OTP factor.">Open self-service</a>
<a class="button-link secondary" href="https://pink.coulomb.social" target="_blank" rel="noreferrer" title="Open privacyIDEA admin for resolver, realm, policy, or fallback token assignment.">Open admin</a>
</div>
</details>
<details class="panel workflow-section" data-section="integrations" open>
<summary><span class="summary-title">3. Integration & Tests</span><span class="state nil" data-section-state="integrations">nil</span></summary>
<div class="section-gate" data-section-gate="integrations">Loading integration gate.</div>
<p class="notice">This section connects the subsystems and shows every console command as a copyable block. Commands still run outside this browser so secret output never enters the control surface.</p>
<div id="integrations-records" class="record-list"></div>
<div class="inline-actions">
<a class="button-link" href="/oidc/start" target="_blank" rel="noreferrer" title="Start the bootstrap-console OIDC authorization flow through KeyCape.">Start OIDC login check</a>
<a class="button-link secondary" href="https://kc.coulomb.social/.well-known/openid-configuration" target="_blank" rel="noreferrer" title="Open KeyCape OIDC discovery JSON.">Open discovery</a>
<a class="button-link secondary" href="https://kc.coulomb.social/healthz" target="_blank" rel="noreferrer" title="Open the KeyCape health endpoint.">Open health</a>
</div>
<div class="choice-list" style="margin-top: 14px;">
<label class="choice"><input id="oidc_login_verified" type="checkbox"><span><strong>OIDC login verified</strong><span>The account can complete the NetKingdom login path through KeyCape after MFA enrollment.</span></span></label>
<label class="choice"><input id="openbao_preflight_passed" type="checkbox"><span><strong>OpenBao preflight passed</strong><span>Status and verification checks completed after custody approval.</span></span></label>
<label class="choice"><input id="openbao_init_output_produced" type="checkbox"><span><strong>Init output produced</strong><span>OpenBao generated unseal shares and the initial root token outside this UI. Do not paste those values here.</span></span></label>
<label class="choice"><input id="openbao_initialized" type="checkbox"><span><strong>Initialized and unsealed</strong><span>The human ceremony completed outside this UI under the approved strategy.</span></span></label>
<label class="choice"><input id="restore_drill_passed" type="checkbox"><span><strong>Restore drill passed</strong><span>Snapshot and isolated restore proof completed before live secrets are migrated.</span></span></label>
</div>
<label class="field" style="margin-top: 14px;">
<span class="label">Root-token disposition</span>
<select id="root_token_disposition" title="Record only what happened to the root token; never record the token value.">
<option value="">Not recorded</option>
<option value="revoked">Revoked after scoped admin works</option>
<option value="offline-sealed">Sealed offline</option>
</select>
</label>
<div id="command-list" class="command-list"></div>
</details>
<details class="panel workflow-section" data-section="runbooks" open>
<summary><span class="summary-title">4. Usecases & Runbooks</span><span class="state nil" data-section-state="runbooks">nil</span></summary>
<div class="section-gate" data-section-gate="runbooks">Loading runbook gate.</div>
<p class="notice">Use these routines when the ceremony path changes, trial secrets are exposed, or custody material must be regenerated. The UI records only non-secret outcomes.</p>
<div id="runbooks-records" class="record-list"></div>
<div class="choice-list">
<label class="choice"><input id="openbao_trial_material_exposed" type="checkbox"><span><strong>Trial key material exposed</strong><span>Init output, unseal shares, or root-token material escaped the custody boundary during a trial.</span></span></label>
<label class="choice"><input id="openbao_compromise_response_complete" type="checkbox"><span><strong>Compromise response complete</strong><span>Exposed material was rotated or the trial environment was reset. No secret values are recorded here.</span></span></label>
<label class="choice"><input id="openbao_unseal_keys_rotated" type="checkbox"><span><strong>New unseal keys generated</strong><span>OpenBao generated replacement unseal shares under the current runbook.</span></span></label>
</div>
<div id="runbook-command-list" class="command-list"></div>
</details>
<details class="panel workflow-section" data-section="artifacts" open>
<summary><span class="summary-title">5. Artefacts & Locations</span><span class="state nil" data-section-state="artifacts">nil</span></summary>
<div class="section-gate" data-section-gate="artifacts">Loading artefact gate.</div>
<p class="notice">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.</p>
<div id="artifacts-records" class="record-list"></div>
<div class="choice-list">
<label class="choice"><input name="custody_mode" value="temporary-single-king" type="radio"><span><strong>Temporary single king</strong><span>Recommended while Railiance is still pre-production. Migration to quorum custody remains planned.</span></span></label>
<label class="choice"><input name="custody_mode" value="two-of-three-ready" type="radio"><span><strong>Two of three ready</strong><span>Use only when independent share holders already exist and can attend the first OpenBao init ceremony.</span></span></label>
<label class="choice"><input name="custody_mode" value="two-of-three-planned" type="radio"><span><strong>Two of three planned</strong><span>Records the target model but does not approve live init yet.</span></span></label>
</div>
<ul class="spec-list" style="margin-top: 14px;">
<li>Recovery material: password recovery, MFA recovery/re-enrollment, custodian age-key recovery, encrypted bundle recovery, and notification contact references.</li>
<li>Custody packet: selected strategy, recovery references, OpenBao init checklist, unseal-share assignment slots, quorum plan, root-token disposition plan, and signature/date line.</li>
</ul>
<div class="choice-list">
<label class="choice"><input name="storage_classes" value="password-safe" type="checkbox"><span><strong>Password safe</strong><span>Credential held in a dedicated vault entry.</span></span></label>
<label class="choice"><input name="storage_classes" value="offline-packet" type="checkbox"><span><strong>Offline packet</strong><span>Recovery material exists outside live systems.</span></span></label>
<label id="hardware-storage-choice" class="choice conditional"><input name="storage_classes" value="hardware-token" type="checkbox"><span><strong>Hardware token storage</strong><span>Shown only when the selected credential policy uses a hardware token.</span></span></label>
<label class="choice"><input id="recovery_confirmed" type="checkbox"><span><strong>Recovery material prepared</strong><span>The items above exist outside this UI. Do not paste secret material here.</span></span></label>
<label class="choice"><input id="custody_packet_prepared" type="checkbox"><span><strong>Custody packet prepared</strong><span>The offline ceremony packet is ready for the selected strategy. No OpenBao root token or unseal share is recorded here.</span></span></label>
</div>
<div class="system-note" style="margin-top: 14px;">Secret capture is enforced by architecture: the control surface does not request secrets, and the gate checks local metadata plus plaintext bootstrap-secret presence.</div>
<div class="field" style="margin-top: 14px;">
<span class="label">Approval phrase for selected strategy</span>
<input id="approval_phrase" type="text" autocomplete="off" placeholder="approve custody mode" title="Type the approval phrase only after the selected strategy, recovery material, and custody packet are ready.">
</div>
<div class="actions">
<button class="secondary" id="save-button" type="button" title="Save the visible non-secret progress fields to local metadata.">Save progress</button>
<button id="approve-button" type="submit" title="Approve the selected custody strategy only after all kit gates are satisfied.">Approve selected strategy</button>
<button class="secondary" id="refresh-button" type="button" title="Reload the local metadata and gate status from disk.">Refresh</button>
</div>
<div id="message" class="message" role="status">Waiting for local approval.</div>
</details>
</form>
<aside>
<section class="panel">
<h2>Bootstrap gates</h2>
<div id="gates" class="gates"></div>
</section>
<section class="panel">
<h2>Key custody</h2>
<div id="key-gates" class="gates"></div>
</section>
<section class="panel">
<h2>Kit gates</h2>
<div id="kit-gates" class="gates"></div>
</section>
</aside>
</div>
</main>
<script>
const fields = [
"bootstrap_mode",
"custodian_age_public_key",
"custodian_age_public_key_fingerprint",
"custodian_age_public_key_confirmed",
"custodian_age_private_key_reference",
"custodian_age_private_key_confirmed",
"credential_label",
"identity_account_home",
"identity_account_reference",
"identity_account_created",
"identity_group_reference",
"identity_group_confirmed",
"password_safe_confirmed",
"oidc_login_verified",
"setup_operator",
"notification_contact",
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email",
"mfa_class",
"mfa_enrollment_source",
"mfa_enrollment_reference",
"mfa_enrolled_confirmed",
"recovery_confirmed",
"custody_packet_prepared",
"openbao_preflight_passed",
"openbao_init_output_produced",
"openbao_initialized",
"openbao_trial_material_exposed",
"openbao_compromise_response_complete",
"openbao_unseal_keys_rotated",
"root_token_disposition",
"restore_drill_passed"
];
const responsibilityFields = [
"setup_operator",
"notification_contact",
"role_setup_operator_email",
"role_platform_custodian_email",
"role_identity_admin_email",
"role_openbao_operator_email",
"role_recovery_custodian_email",
"role_future_quorum_email"
];
let currentMetadata = {};
function setMessage(text, kind) {
const element = document.getElementById("message");
element.textContent = text;
element.className = "message" + (kind ? " " + kind : "");
}
function renderGates(target, gates) {
const root = document.getElementById(target);
root.replaceChildren();
for (const gate of gates) {
const row = document.createElement("div");
row.className = "gate " + gate.status;
const status = document.createElement("div");
status.className = "pill";
status.textContent = gate.status;
const body = document.createElement("div");
const title = document.createElement("div");
title.className = "gate-title";
title.textContent = gate.name;
const reason = document.createElement("div");
reason.className = "gate-reason";
reason.textContent = gate.reason;
body.append(title, reason);
row.append(status, body);
root.append(row);
}
}
function makeStateBadge(state) {
const badge = document.createElement("span");
const value = state || "nil";
badge.className = "state " + value;
badge.textContent = value;
return badge;
}
function makeRoleChip(role, email) {
const chip = document.createElement("span");
chip.className = "role-chip";
chip.textContent = role || "unassigned";
chip.title = email ? role + " -> " + email : role + " has no designated email";
return chip;
}
function renderRecords(target, rows) {
const root = document.getElementById(target);
root.replaceChildren();
for (const record of rows || []) {
const row = document.createElement("div");
row.className = "record-row";
row.append(makeStateBadge(record.state));
const identity = document.createElement("div");
const name = document.createElement("div");
name.className = "record-name";
name.textContent = record.name;
const description = document.createElement("div");
description.className = "record-description";
description.textContent = record.description;
identity.append(name, description);
const context = document.createElement("div");
context.className = "record-context";
const subsystem = document.createElement("div");
subsystem.className = "record-meta";
subsystem.textContent = record.subsystem;
const responsibility = document.createElement("div");
responsibility.append(makeRoleChip(record.responsibility, record.email));
context.append(subsystem, responsibility);
const location = document.createElement("div");
location.className = "record-meta";
location.textContent = record.location;
row.append(identity, context, location);
root.append(row);
}
}
function renderSectionGates(gates) {
for (const gate of gates || []) {
const badge = document.querySelector(`[data-section-state='${gate.key}']`);
if (badge) {
badge.className = "state " + gate.status;
badge.textContent = gate.status;
}
const message = document.querySelector(`[data-section-gate='${gate.key}']`);
if (message) {
message.textContent = gate.reason;
}
}
}
function renderCommands(target, commands) {
const root = document.getElementById(target);
root.replaceChildren();
for (const item of commands || []) {
const row = document.createElement("div");
row.className = "command-row";
const head = document.createElement("div");
head.className = "command-head";
const title = document.createElement("div");
const name = document.createElement("div");
name.className = "record-name";
name.textContent = item.name;
const description = document.createElement("div");
description.className = "record-description";
description.textContent = item.description;
const statusReason = document.createElement("div");
statusReason.className = "record-description";
statusReason.textContent = item.status_reason || "";
title.append(name, description, statusReason);
const button = document.createElement("button");
button.className = "copy-button secondary";
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";
commandActions.append(makeStateBadge(item.status), button);
head.append(title, commandActions);
const code = document.createElement("code");
code.className = "command-code";
code.textContent = item.command;
row.append(head, code);
root.append(row);
}
}
function fillForm(metadata) {
for (const id of fields) {
const element = document.getElementById(id);
if (!element) continue;
if (element.type === "checkbox") {
element.checked = metadata[id] === true;
} else {
element.value = metadata[id] || "";
}
}
const storage = metadata.storage_classes || [];
document.querySelectorAll("[name='storage_classes']").forEach((input) => {
input.checked = storage.includes(input.value);
});
document.querySelectorAll("[name='custody_mode']").forEach((input) => {
input.checked = false;
});
const mode = metadata.custody_mode || "";
const selected = document.querySelector(`[name='custody_mode'][value='${mode}']`);
if (selected) selected.checked = true;
syncConditionalHardware();
}
function fillResponsibilityFields(metadata) {
for (const id of responsibilityFields) {
const element = document.getElementById(id);
if (element) element.value = metadata[id] || "";
}
}
function responsibilityPayload() {
const payload = {};
for (const id of responsibilityFields) {
payload[id] = document.getElementById(id).value.trim();
}
return payload;
}
function setResponsibilityDirty(dirty) {
document.getElementById("responsibility-save-button").disabled = !dirty;
document.getElementById("responsibility-cancel-button").disabled = !dirty;
const badge = document.getElementById("responsibility-edit-state");
badge.className = "state " + (dirty ? "redo" : "set");
badge.textContent = dirty ? "redo" : "set";
}
function syncConditionalHardware() {
const source = document.getElementById("mfa_class");
const row = document.getElementById("hardware-storage-choice");
if (!source || !row) return;
const input = row.querySelector("input");
const visible = source.value === "hardware-token" || (input && input.checked);
row.classList.toggle("visible", visible);
}
async function loadStatus() {
const response = await fetch("/api/status");
const data = await response.json();
document.getElementById("stage").textContent = data.stage;
document.getElementById("next-action").textContent = data.next_action;
document.getElementById("metadata-path").textContent = data.metadata_path;
renderGates("gates", data.gates);
renderGates("key-gates", data.key_custody_gates);
renderGates("kit-gates", data.kit_gates);
renderSectionGates(data.section_gates);
renderRecords("roles-records", data.roles);
renderRecords("subsystems-records", data.subsystems);
renderRecords("integrations-records", data.integrations);
renderRecords("runbooks-records", data.runbooks);
renderRecords("artifacts-records", data.artifacts);
renderCommands("command-list", data.commands);
renderCommands("runbook-command-list", data.runbook_commands);
currentMetadata = data.metadata || {};
fillForm(currentMetadata);
setResponsibilityDirty(false);
}
function approvalPayload() {
const storage = Array.from(document.querySelectorAll("[name='storage_classes']:checked"))
.map((input) => input.value);
const mode = document.querySelector("[name='custody_mode']:checked");
return {
bootstrap_mode: document.getElementById("bootstrap_mode").value,
custodian_age_public_key: document.getElementById("custodian_age_public_key").value.trim(),
custodian_age_public_key_confirmed: document.getElementById("custodian_age_public_key_confirmed").checked,
custodian_age_private_key_reference: document.getElementById("custodian_age_private_key_reference").value.trim(),
custodian_age_private_key_confirmed: document.getElementById("custodian_age_private_key_confirmed").checked,
credential_label: document.getElementById("credential_label").value.trim(),
identity_account_home: document.getElementById("identity_account_home").value.trim(),
identity_account_reference: document.getElementById("identity_account_reference").value.trim(),
identity_account_created: document.getElementById("identity_account_created").checked,
identity_group_reference: document.getElementById("identity_group_reference").value.trim(),
identity_group_confirmed: document.getElementById("identity_group_confirmed").checked,
password_safe_confirmed: document.getElementById("password_safe_confirmed").checked,
oidc_login_verified: document.getElementById("oidc_login_verified").checked,
setup_operator: document.getElementById("setup_operator").value.trim(),
notification_contact: document.getElementById("notification_contact").value.trim(),
role_setup_operator_email: document.getElementById("role_setup_operator_email").value.trim(),
role_platform_custodian_email: document.getElementById("role_platform_custodian_email").value.trim(),
role_identity_admin_email: document.getElementById("role_identity_admin_email").value.trim(),
role_openbao_operator_email: document.getElementById("role_openbao_operator_email").value.trim(),
role_recovery_custodian_email: document.getElementById("role_recovery_custodian_email").value.trim(),
role_future_quorum_email: document.getElementById("role_future_quorum_email").value.trim(),
mfa_class: document.getElementById("mfa_class").value,
mfa_enrollment_source: document.getElementById("mfa_enrollment_source").value,
mfa_enrollment_reference: document.getElementById("mfa_enrollment_reference").value.trim(),
mfa_enrolled_confirmed: document.getElementById("mfa_enrolled_confirmed").checked,
storage_classes: storage,
recovery_confirmed: document.getElementById("recovery_confirmed").checked,
custody_packet_prepared: document.getElementById("custody_packet_prepared").checked,
custody_mode: mode ? mode.value : "",
openbao_preflight_passed: document.getElementById("openbao_preflight_passed").checked,
openbao_init_output_produced: document.getElementById("openbao_init_output_produced").checked,
openbao_initialized: document.getElementById("openbao_initialized").checked,
openbao_trial_material_exposed: document.getElementById("openbao_trial_material_exposed").checked,
openbao_compromise_response_complete: document.getElementById("openbao_compromise_response_complete").checked,
openbao_unseal_keys_rotated: document.getElementById("openbao_unseal_keys_rotated").checked,
root_token_disposition: document.getElementById("root_token_disposition").value,
restore_drill_passed: document.getElementById("restore_drill_passed").checked,
approval_phrase: document.getElementById("approval_phrase").value,
approved_by: document.getElementById("setup_operator").value.trim()
};
}
document.getElementById("mfa_class").addEventListener("change", syncConditionalHardware);
document.getElementById("responsibility-editor").addEventListener("input", () => {
setResponsibilityDirty(true);
});
document.getElementById("responsibility-cancel-button").addEventListener("click", () => {
fillResponsibilityFields(currentMetadata);
setResponsibilityDirty(false);
document.getElementById("responsibility-editor").open = false;
});
document.getElementById("responsibility-save-button").addEventListener("click", async () => {
const button = document.getElementById("responsibility-save-button");
button.disabled = true;
try {
const response = await fetch("/api/save-progress", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(responsibilityPayload())
});
const result = await response.json();
if (!response.ok) {
setMessage("Save failed:\\n" + (result.errors || []).map((item) => "- " + item).join("\\n"), "error");
setResponsibilityDirty(true);
return;
}
await loadStatus();
document.getElementById("responsibility-editor").open = false;
setMessage("Responsibilities saved. No secrets were recorded.", "ok");
} catch (error) {
setMessage("Save failed: " + error.message, "error");
setResponsibilityDirty(true);
}
});
document.addEventListener("click", async (event) => {
const button = event.target.closest(".copy-button");
if (!button) return;
const command = button.dataset.command || "";
try {
await navigator.clipboard.writeText(command);
button.textContent = "Copied";
setTimeout(() => { button.textContent = "Copy"; }, 1000);
} catch (error) {
setMessage("Copy failed: " + error.message, "error");
}
});
document.getElementById("approval-form").addEventListener("submit", async (event) => {
event.preventDefault();
const button = document.getElementById("approve-button");
button.disabled = true;
try {
const response = await fetch("/api/approve-custody", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(approvalPayload())
});
const result = await response.json();
if (!response.ok) {
setMessage("Not approved:\\n" + (result.errors || []).map((item) => "- " + item).join("\\n"), "error");
renderGates("kit-gates", result.kit_gates || []);
return;
}
document.getElementById("approval_phrase").value = "";
await loadStatus();
setMessage("Selected custody strategy approved. OpenBao init remains a separate human ceremony.", "ok");
} catch (error) {
setMessage("Request failed: " + error.message, "error");
} finally {
button.disabled = false;
}
});
document.getElementById("save-button").addEventListener("click", async () => {
const button = document.getElementById("save-button");
button.disabled = true;
try {
const response = await fetch("/api/save-progress", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(approvalPayload())
});
const result = await response.json();
if (!response.ok) {
setMessage("Save failed:\\n" + (result.errors || []).map((item) => "- " + item).join("\\n"), "error");
return;
}
await loadStatus();
setMessage("Progress saved. No secrets were recorded.", "ok");
} catch (error) {
setMessage("Save failed: " + error.message, "error");
} finally {
button.disabled = false;
}
});
document.getElementById("refresh-button").addEventListener("click", () => {
loadStatus().then(() => setMessage("Status refreshed.", ""));
});
loadStatus().catch((error) => setMessage("Load failed: " + error.message, "error"));
</script>
</body>
</html>
"""
def make_ui_handler(metadata_path: Path) -> type[BaseHTTPRequestHandler]:
class SecurityBootstrapUIHandler(BaseHTTPRequestHandler):
server_version = "SecurityBootstrapUI/1.0"
def log_message(self, format: str, *args: Any) -> None:
print(f"{self.address_string()} - {format % args}")
def send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
body = json.dumps(payload, indent=2).encode("utf-8")
self.send_response(status.value)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def send_html(self, status: HTTPStatus, body: str) -> None:
encoded = body.encode("utf-8")
self.send_response(status.value)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/" or parsed.path == "/index.html":
self.send_html(HTTPStatus.OK, ui_html())
return
if parsed.path == "/api/status":
data = load_metadata(metadata_path)
if not data:
data = metadata_template()
self.send_json(HTTPStatus.OK, status_payload(data, metadata_path))
return
if parsed.path == "/oidc/start":
host = self.headers.get("Host", "127.0.0.1:8876")
self.send_response(HTTPStatus.FOUND.value)
self.send_header("Location", local_oidc_start_url(host))
self.send_header("Cache-Control", "no-store")
self.end_headers()
return
if parsed.path == "/oidc/callback":
host = self.headers.get("Host", "127.0.0.1:8876")
self.send_html(HTTPStatus.OK, oidc_result_html(parsed.query, host))
return
self.send_error(HTTPStatus.NOT_FOUND.value)
def do_POST(self) -> None:
if self.path not in {"/api/approve-custody", "/api/save-progress"}:
self.send_error(HTTPStatus.NOT_FOUND.value)
return
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
self.send_json(HTTPStatus.BAD_REQUEST, {"errors": ["Invalid content length."]})
return
if length > 65536:
self.send_json(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, {"errors": ["Request too large."]})
return
try:
payload = json.loads(self.rfile.read(length) or b"{}")
except json.JSONDecodeError as exc:
self.send_json(HTTPStatus.BAD_REQUEST, {"errors": [f"Invalid JSON: {exc}"]})
return
if not isinstance(payload, dict):
self.send_json(HTTPStatus.BAD_REQUEST, {"errors": ["JSON body must be an object."]})
return
existing = load_metadata(metadata_path)
if self.path == "/api/save-progress":
saved = save_progress_metadata(existing, payload)
write_metadata(metadata_path, saved)
response = status_payload(saved, metadata_path)
response["message"] = "Progress saved."
self.send_json(HTTPStatus.OK, response)
return
approval_phrase = str(payload.get("approval_phrase", ""))
approved_by = str(payload.get("approved_by", ""))
approved, errors = approve_custody_metadata(existing, payload, approval_phrase, approved_by)
if errors:
response = status_payload(approved, metadata_path)
response["errors"] = errors
self.send_json(HTTPStatus.BAD_REQUEST, response)
return
write_metadata(metadata_path, approved)
response = status_payload(approved, metadata_path)
response["message"] = "Selected custody strategy approved."
self.send_json(HTTPStatus.OK, response)
return SecurityBootstrapUIHandler
def serve_web_ui(args: argparse.Namespace) -> int:
metadata_path = args.metadata or DEFAULT_METADATA_PATH
handler = make_ui_handler(metadata_path)
httpd = ThreadingHTTPServer((args.host, args.port), handler)
host = html.escape(args.host)
print("SECURITY BOOTSTRAP UI")
print("")
print(f"URL: http://{host}:{args.port}")
print(f"Metadata: {metadata_path}")
print("")
print("Local non-secret custody approval only. Press Ctrl+C to stop.")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("")
print("Stopped.")
finally:
httpd.server_close()
return 0
def refuse_live_init() -> int:
print("REFUSED")
print("")
print("This console does not run bao operator init.")
print("Use the human-attended Railiance OpenBao ceremony after all gates pass.")
return 2
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Non-secret NetKingdom security bootstrap console.",
)
parser.add_argument(
"--metadata",
type=Path,
help="Optional non-secret metadata JSON file.",
)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("status", help="Show trust stage, gates, and next safe action.")
sub.add_parser("king-kit", help="Print king credential kit checklist.")
sub.add_parser("validate-king-kit", help="Validate non-secret king credential metadata.")
approve = sub.add_parser("approve-custody-mode", help="Approve a live-init-ready custody mode.")
approve.add_argument(
"--mode",
choices=sorted(VALID_CUSTODY_MODES),
default="temporary-single-king",
help="Custody mode to record. two-of-three-planned cannot approve live init.",
)
approve.add_argument("--credential-label", dest="credential_label")
approve.add_argument("--setup-operator", dest="setup_operator")
approve.add_argument("--notification-contact", dest="notification_contact")
approve.add_argument("--storage-class", dest="storage_class", action="append")
approve.add_argument("--mfa-class", dest="mfa_class", choices=sorted(VALID_MFA_CLASSES))
approve.add_argument("--mfa-enrolled-confirmed", action="store_true")
approve.add_argument(
"--mfa-enrollment-source",
choices=sorted(VALID_MFA_ENROLLMENT_SOURCES),
default=None,
)
approve.add_argument("--mfa-enrollment-reference")
approve.add_argument("--recovery-confirmed", action="store_true")
approve.add_argument("--custody-packet-prepared", action="store_true")
approve.add_argument("--no-secret-capture-confirmed", action="store_true")
approve.add_argument("--approval-phrase", default="")
approve.add_argument("--approved-by", default="")
approve.add_argument("--notes")
approve.add_argument(
"--yes",
action="store_true",
help=f'Use the required approval phrase "{APPROVAL_PHRASE}" non-interactively.',
)
sub.add_parser("custody-packet", help="Print blank offline custody packet template.")
sub.add_parser("handover-checklist", help="Print handover and cleanup checklist.")
sub.add_parser("metadata-template", help="Print non-secret metadata JSON template.")
sub.add_parser("refuse-live-init", help="Explain why live OpenBao init is refused.")
web = sub.add_parser("web-ui", help="Serve a local custody approval UI.")
web.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to localhost.")
web.add_argument("--port", type=int, default=8765, help="Bind port. Defaults to 8765.")
preflight = sub.add_parser("openbao-preflight", help="Show or run safe OpenBao preflight.")
preflight.add_argument(
"--railiance-path",
default="../railiance-platform",
help="Path to railiance-platform repo.",
)
preflight.add_argument(
"--run",
action="store_true",
help="Run safe preflight targets. Does not run OpenBao init.",
)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
data = load_metadata(args.metadata)
if args.command == "status":
print_status(data)
return 0
if args.command == "king-kit":
print_king_kit()
return 0
if args.command == "validate-king-kit":
return print_validate_king_kit(data)
if args.command == "approve-custody-mode":
return print_approve_custody_mode(args, data)
if args.command == "custody-packet":
print_custody_packet()
return 0
if args.command == "handover-checklist":
print_handover_checklist()
return 0
if args.command == "metadata-template":
print(json.dumps(metadata_template(), indent=2))
return 0
if args.command == "openbao-preflight":
return print_openbao_preflight(args)
if args.command == "web-ui":
return serve_web_ui(args)
if args.command == "refuse-live-init":
return refuse_live_init()
parser.error(f"unknown command: {args.command}")
return 2
if __name__ == "__main__":
raise SystemExit(main())