generated from coulomb/repo-seed
1260 lines
45 KiB
Python
Executable File
1260 lines
45 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 html
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
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"}
|
|
|
|
|
|
@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 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(
|
|
"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 password-safe, offline-packet, hardware-token, or a combination.",
|
|
),
|
|
Gate(
|
|
"Second factor",
|
|
"done" if second_factor_ready(data) else "blocked",
|
|
second_factor_reason(data),
|
|
),
|
|
Gate(
|
|
"Recovery material",
|
|
"done" if yes(data, "recovery_confirmed") else "blocked",
|
|
"Confirm recovery material exists without recording values.",
|
|
),
|
|
Gate(
|
|
"Custody packet",
|
|
"done" if yes(data, "custody_packet_prepared") else "blocked",
|
|
"Prepare the offline custody packet.",
|
|
),
|
|
Gate(
|
|
"No secret capture",
|
|
"done" if yes(data, "no_secret_capture_confirmed") else "blocked",
|
|
"Confirm no secret values were stored in metadata, Git, State Hub, chat, tickets, or email.",
|
|
),
|
|
Gate(
|
|
"Custody mode",
|
|
"done" if custody_mode in VALID_CUSTODY_MODES else "blocked",
|
|
"Approve temporary-single-king, two-of-three-planned, or two-of-three-ready.",
|
|
),
|
|
]
|
|
|
|
|
|
def king_kit_ready(data: dict[str, Any]) -> bool:
|
|
gates = kit_validation(data)
|
|
required = [gate for gate in gates if gate.name != "Custody mode"]
|
|
return all(gate.status == "done" for gate in required)
|
|
|
|
|
|
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 "Mode is selected but not yet explicitly approved."
|
|
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 mode",
|
|
"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 next_action(gates: list[Gate]) -> str:
|
|
for gate in gates:
|
|
if gate.status == "blocked":
|
|
if gate.name == "King credential kit":
|
|
return "Define king credential kit"
|
|
if gate.name == "Custody mode":
|
|
return "Choose custody mode"
|
|
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:
|
|
gates = build_gates(data)
|
|
print("SECURITY BOOTSTRAP")
|
|
print("")
|
|
print("Stage")
|
|
print(derive_stage(data))
|
|
print("")
|
|
print("Next safe action")
|
|
print(next_action(gates))
|
|
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",
|
|
"setup_operator",
|
|
"notification_contact",
|
|
"mfa_class",
|
|
"mfa_enrollment_source",
|
|
"mfa_enrollment_reference",
|
|
"custody_mode",
|
|
"notes",
|
|
)
|
|
for field in text_fields:
|
|
if field in payload and payload[field] is not None:
|
|
data[field] = str(payload[field]).strip()
|
|
if "storage_classes" in payload:
|
|
data["storage_classes"] = normalize_storage_classes(payload["storage_classes"])
|
|
for field in (
|
|
"recovery_confirmed",
|
|
"custody_packet_prepared",
|
|
"no_secret_capture_confirmed",
|
|
"mfa_enrolled_confirmed",
|
|
):
|
|
if field in payload:
|
|
data[field] = payload[field] is True
|
|
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 custody mode.')
|
|
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 mode":
|
|
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",
|
|
"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",
|
|
):
|
|
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 {
|
|
"credential_label": "platform-root",
|
|
"setup_operator": "tegwick",
|
|
"notification_contact": "bernd.worsch@gmail.com",
|
|
"storage_classes": ["password-safe", "offline-packet"],
|
|
"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": "",
|
|
"openbao_preflight_passed": False,
|
|
"openbao_initialized": 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 status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]:
|
|
gates = build_gates(data)
|
|
return {
|
|
"metadata_path": str(metadata_path),
|
|
"stage": derive_stage(data),
|
|
"next_action": next_action(gates),
|
|
"gates": [gate_payload(gate) for gate in gates],
|
|
"kit_gates": [gate_payload(gate) for gate in kit_validation(data)],
|
|
"metadata": data,
|
|
"approval_phrase": APPROVAL_PHRASE,
|
|
"custody_approval_modes": sorted(CUSTODY_APPROVAL_MODES),
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
.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;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
align-items: center;
|
|
margin-top: 16px;
|
|
}
|
|
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:disabled {
|
|
cursor: wait;
|
|
opacity: 0.65;
|
|
}
|
|
.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;
|
|
}
|
|
@media (max-width: 820px) {
|
|
header { padding: 18px; }
|
|
main { padding: 16px; }
|
|
.topline, .layout, .grid { 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>Security bootstrap custody approval</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">
|
|
<section class="panel">
|
|
<h2>King credential</h2>
|
|
<p class="notice">Local non-secret metadata only. OpenBao initialization stays manual.</p>
|
|
<div class="grid">
|
|
<label class="field">
|
|
<span class="label">Credential label</span>
|
|
<input id="credential_label" type="text" autocomplete="off">
|
|
</label>
|
|
<label class="field">
|
|
<span class="label">Setup operator</span>
|
|
<input id="setup_operator" type="text" autocomplete="off">
|
|
</label>
|
|
<label class="field">
|
|
<span class="label">Notification contact</span>
|
|
<input id="notification_contact" type="text" autocomplete="off">
|
|
</label>
|
|
<label class="field">
|
|
<span class="label">Second factor</span>
|
|
<select id="mfa_class">
|
|
<option value="">Select</option>
|
|
<option value="totp">TOTP</option>
|
|
<option value="webauthn">WebAuthn</option>
|
|
<option value="hardware-token">Hardware token</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Second factor enrollment</h2>
|
|
<p class="notice">The QR code or setup key belongs to the authority that verifies login. This UI records confirmation only.</p>
|
|
<div class="grid">
|
|
<label class="field">
|
|
<span class="label">Enrollment source</span>
|
|
<select id="mfa_enrollment_source">
|
|
<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">Reference</span>
|
|
<input id="mfa_enrollment_reference" type="text" autocomplete="off" placeholder="provider or vault entry label">
|
|
</label>
|
|
</div>
|
|
<div class="choice-list" style="margin-top: 14px;">
|
|
<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>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Storage and recovery</h2>
|
|
<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 class="choice"><input name="storage_classes" value="hardware-token" type="checkbox"><span><strong>Hardware token</strong><span>Custody includes hardware-backed access.</span></span></label>
|
|
<label class="choice"><input id="recovery_confirmed" type="checkbox"><span><strong>Recovery material confirmed</strong><span>No values recorded here.</span></span></label>
|
|
<label class="choice"><input id="custody_packet_prepared" type="checkbox"><span><strong>Custody packet prepared</strong><span>Offline packet is ready for the ceremony.</span></span></label>
|
|
<label class="choice"><input id="no_secret_capture_confirmed" type="checkbox"><span><strong>No secret capture</strong><span>No secrets in Git, State Hub, chat, tickets, email, or screenshots.</span></span></label>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Custody mode</h2>
|
|
<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.</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 when independent shares already exist.</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 intent but does not approve live init.</span></span></label>
|
|
</div>
|
|
<div class="field" style="margin-top: 14px;">
|
|
<span class="label">Approval phrase</span>
|
|
<input id="approval_phrase" type="text" autocomplete="off" placeholder="approve custody mode">
|
|
</div>
|
|
<div class="actions">
|
|
<button id="approve-button" type="submit">Approve custody mode</button>
|
|
<button class="secondary" id="refresh-button" type="button">Refresh</button>
|
|
</div>
|
|
<div id="message" class="message" role="status">Waiting for local approval.</div>
|
|
</section>
|
|
</form>
|
|
|
|
<aside>
|
|
<section class="panel">
|
|
<h2>Bootstrap gates</h2>
|
|
<div id="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 = [
|
|
"credential_label",
|
|
"setup_operator",
|
|
"notification_contact",
|
|
"mfa_class",
|
|
"mfa_enrollment_source",
|
|
"mfa_enrollment_reference",
|
|
"mfa_enrolled_confirmed",
|
|
"recovery_confirmed",
|
|
"custody_packet_prepared",
|
|
"no_secret_capture_confirmed"
|
|
];
|
|
|
|
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 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);
|
|
});
|
|
const mode = metadata.custody_mode || "temporary-single-king";
|
|
const selected = document.querySelector(`[name='custody_mode'][value='${mode}']`);
|
|
if (selected) selected.checked = true;
|
|
}
|
|
|
|
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("kit-gates", data.kit_gates);
|
|
fillForm(data.metadata || {});
|
|
}
|
|
|
|
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 {
|
|
credential_label: document.getElementById("credential_label").value.trim(),
|
|
setup_operator: document.getElementById("setup_operator").value.trim(),
|
|
notification_contact: document.getElementById("notification_contact").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,
|
|
no_secret_capture_confirmed: document.getElementById("no_secret_capture_confirmed").checked,
|
|
custody_mode: mode ? mode.value : "",
|
|
approval_phrase: document.getElementById("approval_phrase").value,
|
|
approved_by: document.getElementById("setup_operator").value.trim()
|
|
};
|
|
}
|
|
|
|
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("Custody mode approved. OpenBao init remains a separate human ceremony.", "ok");
|
|
} catch (error) {
|
|
setMessage("Request 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 do_GET(self) -> None:
|
|
if self.path == "/" or self.path == "/index.html":
|
|
body = ui_html().encode("utf-8")
|
|
self.send_response(HTTPStatus.OK.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(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
return
|
|
if self.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
|
|
self.send_error(HTTPStatus.NOT_FOUND.value)
|
|
|
|
def do_POST(self) -> None:
|
|
if self.path != "/api/approve-custody":
|
|
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)
|
|
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"] = "Custody mode 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())
|