2026-05-24 09:26:02 +02:00
#!/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
2026-05-24 17:04:15 +02:00
import base64
import hashlib
2026-05-24 09:26:02 +02:00
import html
import json
2026-05-25 18:48:23 +02:00
import shlex
2026-05-24 09:26:02 +02:00
import subprocess
import sys
2026-05-24 17:04:15 +02:00
import urllib . error
import urllib . parse
import urllib . request
2026-05-24 09:26:02 +02:00
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 "
2026-05-26 01:17:42 +02:00
STAGE_ORDER = ( " S1 " , " S2 " , " S3 " , " S4 " , " S5 " , " S6 " )
2026-05-24 09:26:02 +02:00
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 " }
2026-05-24 17:04:15 +02:00
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 "
2026-05-24 09:26:02 +02:00
@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. "
2026-05-24 17:04:15 +02:00
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 " )
2026-05-25 01:25:47 +02:00
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. "
2026-05-24 17:04:15 +02:00
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. " ,
) ,
]
2026-05-24 09:26:02 +02:00
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. " ,
) ,
2026-05-24 17:04:15 +02:00
Gate (
" Identity account " ,
" done " if identity_account_ready ( data ) else " blocked " ,
identity_account_reason ( data ) ,
) ,
2026-05-24 09:26:02 +02:00
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 " ,
2026-05-25 01:25:47 +02:00
" Select where the credential is held; hardware is optional policy, not a default requirement. " ,
2026-05-24 09:26:02 +02:00
) ,
2026-05-24 17:04:15 +02:00
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. " ,
) ,
2026-05-24 09:26:02 +02:00
Gate (
" Second factor " ,
" done " if second_factor_ready ( data ) else " blocked " ,
second_factor_reason ( data ) ,
) ,
2026-05-24 17:04:15 +02:00
Gate (
" Identity login path " ,
" done " if identity_login_ready ( data ) else " blocked " ,
" Verify the dedicated account can complete the NetKingdom login path. " ,
) ,
2026-05-25 02:02:14 +02:00
Gate (
" Custody strategy selected " ,
" done " if custody_mode in VALID_CUSTODY_MODES else " blocked " ,
" Choose how the OpenBao init ceremony will be controlled. " ,
) ,
2026-05-24 09:26:02 +02:00
Gate (
" Recovery material " ,
2026-05-25 01:25:47 +02:00
" done " if recovery_material_ready ( data ) else " blocked " ,
recovery_material_reason ( data ) ,
2026-05-24 09:26:02 +02:00
) ,
Gate (
" Custody packet " ,
2026-05-25 01:25:47 +02:00
" done " if custody_packet_ready ( data ) else " blocked " ,
custody_packet_reason ( data ) ,
2026-05-24 09:26:02 +02:00
) ,
Gate (
2026-05-25 01:25:47 +02:00
" Control-surface secret boundary " ,
" done " if secret_boundary_ready ( data ) else " blocked " ,
secret_boundary_reason ( data ) ,
2026-05-24 09:26:02 +02:00
) ,
]
def king_kit_ready ( data : dict [ str , Any ] ) - > bool :
gates = kit_validation ( data )
2026-05-25 02:02:14 +02:00
return all ( gate . status == " done " for gate in gates )
2026-05-24 09:26:02 +02:00
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 " ) :
2026-05-25 01:25:47 +02:00
return " Strategy is selected; explicit approval is still pending. "
2026-05-24 09:26:02 +02:00
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 " ) :
2026-05-26 01:17:42 +02:00
return " S6 - Reopen under custody "
if yes ( data , " cleanup_complete " ) or yes ( data , " openbao_oidc_admin_login_verified " ) :
return " S5 - Cleanup and hardening "
if yes ( data , " openbao_initial_config_applied " ) :
return " S4 - Admin identity integration "
2026-05-24 09:26:02 +02:00
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
2026-05-25 23:36:45 +02:00
def stage_payloads ( data : dict [ str , Any ] ) - > list [ dict [ str , str ] ] :
current = derive_stage ( data ) . split ( " - " , 1 ) [ 0 ]
try :
current_index = STAGE_ORDER . index ( current )
except ValueError :
current_index = 0
rows = [
(
" S1 " ,
" Low-trust assembly " ,
" Install and connect subsystems using low-trust setup accounts and non-secret metadata. " ,
" Complete the king credential kit. " ,
) ,
(
" S2 " ,
" King credential preparation " ,
" Create platform-root, enroll MFA, approve custody strategy, and prepare recovery material. " ,
" Run OpenBao preflight and init under the selected custody strategy. " ,
) ,
(
" S3 " ,
" OpenBao bootstrap " ,
2026-05-26 01:17:42 +02:00
" Initialize, unseal, rotate trial material, configure OpenBao, and create a temporary admin bridge. " ,
" Bind normal OpenBao admin access to NetKingdom identity. " ,
2026-05-25 23:36:45 +02:00
) ,
(
" S4 " ,
2026-05-26 01:17:42 +02:00
" Admin identity integration " ,
" Register OpenBao with KeyCape and map verified NetKingdom admin claims to the platform-admin policy. " ,
" Verify OIDC-backed OpenBao admin login. " ,
) ,
(
" S5 " ,
2026-05-25 23:36:45 +02:00
" Cleanup and hardening " ,
2026-05-26 01:17:42 +02:00
" Retire bootstrap-era access, resolve taint, record root-token disposition, prove restore, and document residual risk. " ,
2026-05-25 23:36:45 +02:00
" Complete cleanup and mark the platform ready to reopen. " ,
) ,
(
2026-05-26 01:17:42 +02:00
" S6 " ,
2026-05-25 23:36:45 +02:00
" Reopen under custody " ,
" Operate under the approved custody model with break-glass and recovery paths known. " ,
" Review related workplans. " ,
) ,
]
payload = [ ]
for index , ( stage_id , name , description , next_step ) in enumerate ( rows ) :
status = " pending "
if index < current_index :
status = " done "
elif index == current_index :
status = " active "
payload . append (
{
" id " : stage_id ,
" name " : name ,
" description " : description ,
" next " : next_step ,
" status " : status ,
}
)
return payload
2026-05-24 09:26:02 +02:00
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 (
2026-05-25 01:25:47 +02:00
" Custody strategy approval " ,
2026-05-24 09:26:02 +02:00
" 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. " ,
) ,
2026-05-25 15:14:59 +02:00
Gate (
" OpenBao initial configuration " ,
(
" done "
if yes ( data , " openbao_initial_config_applied " )
else " human "
if yes ( data , " openbao_initialized " )
else " blocked "
) ,
" Apply first auth, mount, and policy configuration; audit may be a declarative follow-up. " ,
) ,
2026-05-26 01:17:42 +02:00
Gate (
" KeyCape OpenBao client " ,
" done " if yes ( data , " openbao_oidc_client_registered " ) else " blocked " ,
" Dedicated OpenBao OIDC client is registered in KeyCape with exact redirect URIs. " ,
) ,
Gate (
" OpenBao OIDC auth " ,
" done " if yes ( data , " openbao_oidc_auth_configured " ) else " blocked " ,
" OpenBao OIDC/JWT auth is configured against KeyCape and maps claims to policy. " ,
) ,
Gate (
" OIDC admin login " ,
" done " if yes ( data , " openbao_oidc_admin_login_verified " ) else " blocked " ,
" platform-root can obtain OpenBao platform-admin access through KeyCape/MFA. " ,
) ,
2026-05-24 09:26:02 +02:00
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. " ,
) ,
2026-05-25 23:36:45 +02:00
Gate (
" Platform reopen " ,
" done " if yes ( data , " platform_reopened " ) else " blocked " ,
" Final operator confirmation that the platform is reopened under custody. " ,
) ,
2026-05-24 09:26:02 +02:00
]
2026-05-25 02:02:14 +02:00
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 "
2026-05-25 13:38:03 +02:00
def next_action (
gates : list [ Gate ] ,
kit_gates : list [ Gate ] | None = None ,
data : dict [ str , Any ] | None = None ,
) - > str :
2026-05-24 09:26:02 +02:00
for gate in gates :
2026-05-25 10:50:24 +02:00
if gate . status == " human " :
if gate . name == " OpenBao init ceremony " :
2026-05-25 13:38:03 +02:00
if data and yes ( data , " openbao_init_output_produced " ) and not yes ( data , " openbao_initialized " ) :
return " Run OpenBao unseal prompt "
2026-05-25 10:50:24 +02:00
return " Run attended OpenBao init ceremony "
return gate . name
2026-05-24 09:26:02 +02:00
if gate . status == " blocked " :
if gate . name == " King credential kit " :
2026-05-25 02:02:14 +02:00
if kit_gates is not None :
return kit_next_action ( kit_gates )
2026-05-24 09:26:02 +02:00
return " Define king credential kit "
2026-05-25 01:25:47 +02:00
if gate . name == " Custody strategy approval " :
return " Approve custody strategy "
2026-05-24 09:26:02 +02:00
if gate . name == " OpenBao preflight " :
return " Run OpenBao preflight "
2026-05-26 01:17:42 +02:00
if gate . name == " KeyCape OpenBao client " :
return " Register OpenBao OIDC client "
if gate . name == " OpenBao OIDC auth " :
return " Configure OpenBao OIDC auth "
if gate . name == " OIDC admin login " :
return " Verify OpenBao OIDC admin login "
2026-05-24 09:26:02 +02:00
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 "
2026-05-25 23:36:45 +02:00
if gate . name == " Platform reopen " :
return " Reopen platform under custody "
2026-05-24 09:26:02 +02:00
return " Review related workplans "
def print_status ( data : dict [ str , Any ] ) - > None :
2026-05-24 17:04:15 +02:00
merged = metadata_template ( )
merged . update ( data )
gates = build_gates ( merged )
key_gates = key_custody_validation ( merged )
2026-05-25 02:02:14 +02:00
kit_gates = kit_validation ( merged )
2026-05-24 17:04:15 +02:00
state = bootstrap_secret_state ( )
2026-05-24 09:26:02 +02:00
print ( " SECURITY BOOTSTRAP " )
print ( " " )
print ( " Stage " )
2026-05-24 17:04:15 +02:00
print ( derive_stage ( merged ) )
2026-05-24 09:26:02 +02:00
print ( " " )
print ( " Next safe action " )
2026-05-25 13:38:03 +02:00
print ( next_action ( gates , kit_gates , merged ) )
2026-05-24 09:26:02 +02:00
print ( " " )
2026-05-24 17:04:15 +02:00
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 ( " " )
2026-05-24 09:26:02 +02:00
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 " ,
2026-05-24 17:04:15 +02:00
" bootstrap_mode " ,
" identity_account_home " ,
" identity_account_reference " ,
" identity_group_reference " ,
" custodian_age_private_key_reference " ,
2026-05-24 09:26:02 +02:00
" setup_operator " ,
" notification_contact " ,
2026-05-25 11:49:51 +02:00
" role_setup_operator_email " ,
" role_platform_custodian_email " ,
" role_identity_admin_email " ,
" role_openbao_operator_email " ,
" role_recovery_custodian_email " ,
" role_future_quorum_email " ,
2026-05-24 09:26:02 +02:00
" mfa_class " ,
" mfa_enrollment_source " ,
" mfa_enrollment_reference " ,
" custody_mode " ,
2026-05-25 02:02:14 +02:00
" root_token_disposition " ,
2026-05-24 09:26:02 +02:00
" notes " ,
)
for field in text_fields :
if field in payload and payload [ field ] is not None :
2026-05-24 17:04:15 +02:00
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 " ] )
2026-05-24 09:26:02 +02:00
if " storage_classes " in payload :
data [ " storage_classes " ] = normalize_storage_classes ( payload [ " storage_classes " ] )
for field in (
2026-05-24 17:04:15 +02:00
" custodian_age_public_key_confirmed " ,
" custodian_age_private_key_confirmed " ,
2026-05-24 09:26:02 +02:00
" recovery_confirmed " ,
" custody_packet_prepared " ,
" no_secret_capture_confirmed " ,
" mfa_enrolled_confirmed " ,
2026-05-24 17:04:15 +02:00
" identity_account_created " ,
" identity_group_confirmed " ,
" oidc_login_verified " ,
" password_safe_confirmed " ,
2026-05-25 02:02:14 +02:00
" openbao_preflight_passed " ,
2026-05-25 13:38:03 +02:00
" openbao_init_output_produced " ,
2026-05-25 02:02:14 +02:00
" openbao_initialized " ,
2026-05-25 14:30:57 +02:00
" openbao_post_unseal_verified " ,
2026-05-25 15:14:59 +02:00
" openbao_initial_config_applied " ,
2026-05-25 13:38:03 +02:00
" openbao_trial_material_exposed " ,
" openbao_compromise_response_complete " ,
" openbao_unseal_keys_rotated " ,
2026-05-25 18:31:48 +02:00
" openbao_emergency_lockdown_drilled " ,
2026-05-26 01:17:42 +02:00
" openbao_oidc_client_registered " ,
" openbao_oidc_auth_configured " ,
" openbao_oidc_admin_login_verified " ,
2026-05-25 02:02:14 +02:00
" restore_drill_passed " ,
2026-05-25 23:36:45 +02:00
" cleanup_complete " ,
" platform_reopened " ,
2026-05-24 09:26:02 +02:00
) :
if field in payload :
data [ field ] = payload [ field ] is True
return data
2026-05-24 17:04:15 +02:00
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
2026-05-24 09:26:02 +02:00
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 :
2026-05-25 01:25:47 +02:00
errors . append ( f ' Type " { APPROVAL_PHRASE } " to approve the selected custody strategy. ' )
2026-05-24 09:26:02 +02:00
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 ) :
2026-05-25 01:25:47 +02:00
if gate . name == " Custody strategy selected " :
2026-05-24 09:26:02 +02:00
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 " ,
2026-05-24 17:04:15 +02:00
" identity_account_home " ,
" identity_account_reference " ,
" identity_group_reference " ,
2026-05-24 09:26:02 +02:00
" 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 " ,
2026-05-24 17:04:15 +02:00
" identity_account_created " ,
" identity_group_confirmed " ,
" oidc_login_verified " ,
" password_safe_confirmed " ,
2026-05-24 09:26:02 +02:00
) :
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 {
2026-05-24 17:04:15 +02:00
" bootstrap_mode " : " custody " ,
" custodian_age_public_key " : " " ,
" custodian_age_public_key_confirmed " : False ,
" custodian_age_private_key_reference " : " " ,
" custodian_age_private_key_confirmed " : False ,
2026-05-24 09:26:02 +02:00
" credential_label " : " platform-root " ,
2026-05-24 17:04:15 +02:00
" identity_account_home " : " lldap " ,
" identity_account_reference " : " " ,
" identity_account_created " : False ,
" identity_group_reference " : " net-kingdom-admins " ,
" identity_group_confirmed " : False ,
2026-05-24 09:26:02 +02:00
" setup_operator " : " tegwick " ,
" notification_contact " : " bernd.worsch@gmail.com " ,
2026-05-25 11:49:51 +02:00
" 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 " : " " ,
2026-05-24 09:26:02 +02:00
" storage_classes " : [ " password-safe " , " offline-packet " ] ,
2026-05-24 17:04:15 +02:00
" password_safe_confirmed " : False ,
2026-05-24 09:26:02 +02:00
" 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 " : " " ,
2026-05-24 17:04:15 +02:00
" oidc_login_verified " : False ,
" metadata_updated_at " : " " ,
" progress_scope " : " " ,
2026-05-24 09:26:02 +02:00
" openbao_preflight_passed " : False ,
2026-05-25 13:38:03 +02:00
" openbao_init_output_produced " : False ,
2026-05-24 09:26:02 +02:00
" openbao_initialized " : False ,
2026-05-25 14:30:57 +02:00
" openbao_post_unseal_verified " : False ,
2026-05-25 15:14:59 +02:00
" openbao_initial_config_applied " : False ,
2026-05-25 13:38:03 +02:00
" openbao_trial_material_exposed " : False ,
" openbao_compromise_response_complete " : False ,
" openbao_unseal_keys_rotated " : False ,
2026-05-25 18:31:48 +02:00
" openbao_emergency_lockdown_drilled " : False ,
2026-05-26 01:17:42 +02:00
" openbao_oidc_client_registered " : False ,
" openbao_oidc_auth_configured " : False ,
" openbao_oidc_admin_login_verified " : False ,
2026-05-24 09:26:02 +02:00
" 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 ,
}
2026-05-25 11:49:51 +02:00
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 "
2026-05-25 14:57:53 +02:00
def openbao_trial_taint ( data : dict [ str , Any ] , relation : str = " downstream " ) - > dict [ str , Any ] :
if not yes ( data , " openbao_trial_material_exposed " ) or yes ( data , " openbao_compromise_response_complete " ) :
return { }
relation_text = " Directly marked " if relation == " direct " else " Downstream "
return {
" tainted " : True ,
" taint_source " : " Trial key material exposed " ,
" taint_reference " : " Usecases & Runbooks / Trial key material exposed " ,
" taint_reason " : (
f " { relation_text } from recorded OpenBao trial key-material exposure. "
" Operator may proceed, but resulting evidence and work should be treated as tainted "
" until rotation, reset, or another compromise response is recorded. "
) ,
}
def add_taint ( row : dict [ str , Any ] , taint : dict [ str , Any ] ) - > dict [ str , Any ] :
if taint :
row . update ( taint )
return row
2026-05-25 11:49:51 +02:00
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 ] ] :
2026-05-25 14:57:53 +02:00
openbao_direct_taint = openbao_trial_taint ( data , " direct " )
2026-05-25 11:49:51 +02:00
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 " ) ) ,
} ,
2026-05-25 14:57:53 +02:00
add_taint (
{
" 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 " ) ) ,
} ,
openbao_direct_taint ,
) ,
2026-05-25 15:14:59 +02:00
add_taint (
{
" name " : " OpenBao initial configuration " ,
" description " : " First auth, mount, and policy configuration after unseal. " ,
" subsystem " : " Railiance OpenBao " ,
" responsibility " : " openbao-ceremony-operator " ,
" email " : role_email ( data , " role_openbao_operator_email " ) ,
" location " : " ../railiance-platform openbao-configure-initial " ,
" state " : state_value ( yes ( data , " openbao_initial_config_applied " ) , yes ( data , " openbao_initialized " ) ) ,
} ,
openbao_trial_taint ( data , " downstream " ) if yes ( data , " openbao_initialized " ) else { } ,
) ,
2026-05-25 11:49:51 +02:00
]
2026-05-26 01:17:42 +02:00
def admin_identity_payloads ( data : dict [ str , Any ] ) - > list [ dict [ str , str ] ] :
downstream_taint = openbao_trial_taint ( data , " downstream " ) if yes ( data , " openbao_initialized " ) else { }
return [
add_taint (
{
" name " : " OpenBao OIDC client " ,
" description " : " Dedicated KeyCape client for OpenBao admin login with exact localhost callback URIs. " ,
" subsystem " : " KeyCape " ,
" responsibility " : " identity-admin " ,
" email " : role_email ( data , " role_identity_admin_email " ) ,
" location " : " clientId=openbao-admin; scopes=openid profile email groups " ,
" state " : state_value ( yes ( data , " openbao_oidc_client_registered " ) , yes ( data , " openbao_initial_config_applied " ) ) ,
} ,
downstream_taint ,
) ,
add_taint (
{
" name " : " OpenBao OIDC auth method " ,
" description " : " OpenBao trusts KeyCape discovery and maps the net-kingdom-admins group to the platform-admin policy. " ,
" subsystem " : " Railiance OpenBao " ,
" responsibility " : " openbao-ceremony-operator " ,
" email " : role_email ( data , " role_openbao_operator_email " ) ,
" location " : " auth/keycape or equivalent OpenBao OIDC/JWT mount " ,
" state " : state_value ( yes ( data , " openbao_oidc_auth_configured " ) , yes ( data , " openbao_oidc_client_registered " ) ) ,
} ,
downstream_taint ,
) ,
add_taint (
{
" name " : " OIDC admin login " ,
" description " : " platform-root can obtain OpenBao platform-admin access through KeyCape MFA instead of a manually minted token. " ,
" subsystem " : " KeyCape -> OpenBao " ,
" responsibility " : " platform-root-custodian " ,
" email " : role_email ( data , " role_platform_custodian_email " ) ,
" location " : " bao login -method=oidc -path=keycape role=platform-admin " ,
" state " : state_value ( yes ( data , " openbao_oidc_admin_login_verified " ) , yes ( data , " openbao_oidc_auth_configured " ) ) ,
} ,
downstream_taint ,
) ,
]
2026-05-25 11:49:51 +02:00
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 " " )
2026-05-25 13:38:03 +02:00
init_output = yes ( data , " openbao_init_output_produced " )
2026-05-25 14:57:53 +02:00
openbao_direct_taint = openbao_trial_taint ( data , " direct " )
2026-05-25 11:49:51 +02:00
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 " ) ) ,
} ,
2026-05-25 14:57:53 +02:00
add_taint (
{
" 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 " ) ) ,
} ,
openbao_direct_taint ,
) ,
add_taint (
{
" 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 " ) ) ,
} ,
openbao_direct_taint ,
) ,
2026-05-25 11:49:51 +02:00
]
2026-05-25 13:13:47 +02:00
def command_payloads ( data : dict [ str , Any ] ) - > list [ dict [ str , str ] ] :
preflight_done = yes ( data , " openbao_preflight_passed " )
custody_approved = custody_mode_approved ( data )
2026-05-25 13:38:03 +02:00
init_output = yes ( data , " openbao_init_output_produced " )
2026-05-25 13:13:47 +02:00
initialized = yes ( data , " openbao_initialized " )
2026-05-25 14:30:57 +02:00
post_unseal_verified = yes ( data , " openbao_post_unseal_verified " )
2026-05-25 15:14:59 +02:00
initial_config_applied = yes ( data , " openbao_initial_config_applied " )
2026-05-25 13:38:03 +02:00
trial_exposed = yes ( data , " openbao_trial_material_exposed " )
2026-05-25 14:57:53 +02:00
response_complete = yes ( data , " openbao_compromise_response_complete " )
openbao_direct_taint = openbao_trial_taint ( data , " direct " )
openbao_downstream_taint = openbao_trial_taint ( data , " downstream " )
2026-05-25 13:13:47 +02:00
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. "
2026-05-25 13:38:03 +02:00
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. "
2026-05-25 13:13:47 +02:00
if not preflight_done :
2026-05-25 13:38:03 +02:00
init_state = " blocked "
init_reason = " OpenBao preflight must pass first. "
2026-05-25 13:13:47 +02:00
2026-05-25 15:14:59 +02:00
config_state = " done " if initial_config_applied else " todo "
config_reason = " Initial configuration is recorded. Root-token disposition remains a separate gate. "
if not initial_config_applied :
config_reason = " Configure OpenBao, then record this non-secret completion flag. "
2026-05-25 14:57:53 +02:00
if trial_exposed and initialized and not response_complete :
2026-05-25 15:14:59 +02:00
if initial_config_applied :
config_reason = " Initial configuration is recorded on a tainted workpath. Complete root-token disposition and compromise response before production trust. "
else :
config_reason = " Tainted by trial key-material exposure. Operator may proceed, but record the taint and complete rotation, reset, or another compromise response before production trust. "
2026-05-25 14:57:53 +02:00
if not initialized :
2026-05-25 13:13:47 +02:00
config_state = " blocked "
2026-05-25 14:57:53 +02:00
config_reason = " OpenBao must be initialized and unsealed first. "
2026-05-25 13:13:47 +02:00
2026-05-25 14:30:57 +02:00
verify_state = " done " if post_unseal_verified else " todo "
verify_reason = " Post-unseal readiness has been verified. "
if not post_unseal_verified :
verify_reason = " Verify filesystem and post-unseal readiness before live secrets move in. "
2026-05-25 13:13:47 +02:00
if not initialized :
verify_state = " blocked "
verify_reason = " OpenBao must be initialized and unsealed first. "
2026-05-25 11:49:51 +02:00
return [
{
" name " : " OpenBao preflight " ,
" description " : " Run safe status and verification checks. Does not initialize OpenBao. " ,
2026-05-25 13:13:47 +02:00
" status " : preflight_state ,
" status_reason " : preflight_reason ,
2026-05-25 11:49:51 +02:00
" command " : (
" python3 tools/security-bootstrap-console/security_bootstrap_console.py "
" openbao-preflight --railiance-path ../railiance-platform --run "
) ,
} ,
2026-05-25 14:57:53 +02:00
add_taint (
{
" 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 " ,
} ,
openbao_direct_taint if init_output or initialized else { } ,
) ,
add_taint (
{
" 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 " ,
} ,
openbao_downstream_taint if initialized else { } ,
) ,
add_taint (
{
" 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 " ,
} ,
openbao_downstream_taint if initialized else { } ,
) ,
2026-05-25 11:49:51 +02:00
]
2026-05-25 13:38:03 +02:00
def runbook_payloads ( data : dict [ str , Any ] ) - > list [ dict [ str , str ] ] :
initialized = yes ( data , " openbao_initialized " )
2026-05-25 18:48:23 +02:00
initial_config_applied = yes ( data , " openbao_initial_config_applied " )
2026-05-25 13:38:03 +02:00
trial_exposed = yes ( data , " openbao_trial_material_exposed " )
response_complete = yes ( data , " openbao_compromise_response_complete " )
2026-05-25 14:57:53 +02:00
openbao_direct_taint = openbao_trial_taint ( data , " direct " )
openbao_downstream_taint = openbao_trial_taint ( data , " downstream " )
2026-05-25 13:38:03 +02:00
2026-05-25 23:10:02 +02:00
key_compromise_location = " Template: record the exposure, choose reset versus rotation, inspect affected paths, and record only the non-secret outcome. "
if trial_exposed and not response_complete :
key_compromise_location = " Active template: trial material is marked exposed; choose reset versus rotation before production trust. "
elif response_complete :
key_compromise_location = " Template retained for future incidents; the current non-secret compromise response is marked complete. "
2026-05-25 13:38:03 +02:00
2026-05-25 23:10:02 +02:00
rotate_location = " Template: unseal OpenBao, start rotate-keys, submit current shares by prompt, route new shares to custody holders, then record confirmation. "
2026-05-25 13:38:03 +02:00
if not initialized :
2026-05-25 23:10:02 +02:00
rotate_location = " Template: prepare now; execution needs an unsealed OpenBao instance and a quorum of current shares. "
lockdown_location = " Template: use a root/sudo-capable token, run emergency seal, confirm Sealed true, then record the drill or incident outcome. "
2026-05-25 18:31:48 +02:00
if not initialized :
2026-05-25 23:10:02 +02:00
lockdown_location = " Template: prepare now; execution only changes availability while OpenBao is unsealed and serving requests. "
2026-05-25 18:31:48 +02:00
2026-05-25 23:10:02 +02:00
restore_location = " Template: prepare workspace, snapshot, encrypt to age recipient, restore in isolation, verify, destroy drill environment, then record evidence. "
2026-05-25 18:48:23 +02:00
if not initial_config_applied :
2026-05-25 23:10:02 +02:00
restore_location = " Template: prepare now; execute after initial OpenBao configuration exists and before live secrets move in. "
2026-05-25 18:48:23 +02:00
2026-05-25 13:38:03 +02:00
return [
2026-05-25 14:57:53 +02:00
add_taint (
{
" 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 ,
2026-05-25 23:10:02 +02:00
" state " : " template " ,
2026-05-25 14:57:53 +02:00
} ,
openbao_direct_taint if trial_exposed and not response_complete else { } ,
) ,
add_taint (
{
" 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 ,
2026-05-25 23:10:02 +02:00
" state " : " template " ,
2026-05-25 14:57:53 +02:00
} ,
openbao_downstream_taint if trial_exposed and not response_complete else { } ,
) ,
2026-05-25 18:31:48 +02:00
add_taint (
{
" name " : " Emergency lock-down " ,
" description " : " Seal Railiance OpenBao to stop access to stored OpenBao assets until a later unseal quorum reopens it. " ,
" subsystem " : " Railiance OpenBao " ,
" responsibility " : " openbao-ceremony-operator " ,
" email " : role_email ( data , " role_openbao_operator_email " ) ,
" location " : lockdown_location ,
2026-05-25 23:10:02 +02:00
" state " : " template " ,
2026-05-25 18:31:48 +02:00
} ,
openbao_downstream_taint if initialized else { } ,
) ,
2026-05-25 18:48:23 +02:00
add_taint (
{
" name " : " Restore drill " ,
" description " : " Prove that Railiance OpenBao can be snapshotted, restored into isolation, unsealed, and verified before production trust. " ,
" subsystem " : " Railiance OpenBao " ,
" responsibility " : " openbao-ceremony-operator " ,
" email " : role_email ( data , " role_openbao_operator_email " ) ,
" location " : restore_location ,
2026-05-25 23:10:02 +02:00
" state " : " template " ,
2026-05-25 18:48:23 +02:00
} ,
openbao_downstream_taint if initialized else { } ,
) ,
2026-05-25 13:38:03 +02:00
]
def runbook_command_payloads ( data : dict [ str , Any ] ) - > list [ dict [ str , str ] ] :
initialized = yes ( data , " openbao_initialized " )
trial_exposed = yes ( data , " openbao_trial_material_exposed " )
response_complete = yes ( data , " openbao_compromise_response_complete " )
2026-05-25 14:57:53 +02:00
openbao_direct_taint = openbao_trial_taint ( data , " direct " )
openbao_downstream_taint = openbao_trial_taint ( data , " downstream " )
2026-05-25 13:38:03 +02:00
2026-05-25 23:10:02 +02:00
def token_prompt_command ( bao_command : str ) - > str :
return (
" printf ' OpenBao token: ' >&2 \n "
" read -rs OPENBAO_TOKEN \n "
" printf ' \\ n ' >&2 \n "
" printf ' %s \\ n ' \" $OPENBAO_TOKEN \" | kubectl exec -i -n openbao openbao-0 -- "
f " sh -c ' read -r BAO_TOKEN; export BAO_TOKEN; { bao_command } ' \n "
" unset OPENBAO_TOKEN "
)
2026-05-25 13:38:03 +02:00
2026-05-25 23:56:55 +02:00
def interactive_token_command ( bao_command : str , prompt_nonce : bool = False ) - > str :
nonce_prompt = (
' printf " Rotation nonce: " >&2 \n '
' read -r ROTATION_NONCE \n '
if prompt_nonce
else " "
)
return (
" kubectl exec -it -n openbao openbao-0 -- sh -lc ' \n "
" restore_tty() { stty echo 2>/dev/null || true; } \n "
" trap restore_tty EXIT INT TERM \n "
f " { nonce_prompt } "
" printf \" OpenBao token: \" >&2 \n "
" stty -echo \n "
" read -r BAO_TOKEN \n "
" stty echo \n "
" printf \" \\ n \" >&2 \n "
" export BAO_TOKEN \n "
f " { bao_command } \n "
" unset BAO_TOKEN \n "
" ' "
)
2026-05-25 23:10:02 +02:00
def action ( name : str , description : str , command : str , taint : dict [ str , str ] | None = None ) - > dict [ str , str ] :
return add_taint (
{
" name " : name ,
" description " : description ,
" command " : command ,
} ,
taint or { } ,
)
2026-05-25 18:31:48 +02:00
2026-05-25 23:10:02 +02:00
seal_command = token_prompt_command ( " bao operator seal " )
audit_list_command = token_prompt_command ( " bao audit list " )
secrets_list_command = token_prompt_command ( " bao secrets list " )
auth_list_command = token_prompt_command ( " bao auth list " )
2026-05-26 00:23:06 +02:00
platform_admin_token_command = token_prompt_command (
" bao token create -policy=platform-admin -period=24h -orphan "
)
2026-05-25 23:56:55 +02:00
rotate_init_command = interactive_token_command (
" bao operator rotate-keys -init -key-shares=3 -key-threshold=2 "
)
2026-05-26 00:09:19 +02:00
rotate_status_command = interactive_token_command ( " bao operator rotate-keys -status " )
2026-05-25 23:56:55 +02:00
rotate_submit_command = interactive_token_command (
' bao operator rotate-keys -nonce= " $ROTATION_NONCE " ' ,
prompt_nonce = True ,
)
rotate_cancel_command = interactive_token_command ( " bao operator rotate-keys -cancel " )
2026-05-25 23:10:02 +02:00
openbao_status_command = " kubectl exec -n openbao openbao-0 -- bao status "
direct_taint = openbao_direct_taint if initialized else { }
downstream_taint = openbao_downstream_taint if initialized else { }
compromise_taint = openbao_downstream_taint if trial_exposed and not response_complete else { }
2026-05-25 18:48:23 +02:00
public_key = extract_age_public_key ( data . get ( " custodian_age_public_key " ) )
quoted_public_key = shlex . quote ( public_key if public_key else " <age-recipient> " )
snapshot_workspace_command = (
' export RESTORE_DRILL_DIR= " $ { RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill} " \n '
' mkdir -p " $RESTORE_DRILL_DIR " \n '
' chmod 700 " $RESTORE_DRILL_DIR " \n '
' printf " Restore drill workspace: %s \\ n " " $RESTORE_DRILL_DIR " '
)
snapshot_command = (
' export RESTORE_DRILL_DIR= " $ { RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill} " \n '
' mkdir -p " $RESTORE_DRILL_DIR " \n '
' chmod 700 " $RESTORE_DRILL_DIR " \n '
" printf ' OpenBao token: ' >&2 \n "
" read -rs OPENBAO_TOKEN \n "
" printf ' \\ n ' >&2 \n "
' printf \' %s \\ n \' " $OPENBAO_TOKEN " | kubectl exec -i -n openbao openbao-0 -- '
" sh -c ' read -r BAO_TOKEN; export BAO_TOKEN; bao operator raft snapshot save /tmp/openbao-raft.snap ' \n "
" unset OPENBAO_TOKEN \n "
' kubectl cp openbao/openbao-0:/tmp/openbao-raft.snap " $RESTORE_DRILL_DIR/openbao-raft.snap " \n '
' kubectl exec -n openbao openbao-0 -- rm -f /tmp/openbao-raft.snap \n '
' sha256sum " $RESTORE_DRILL_DIR/openbao-raft.snap " | tee " $RESTORE_DRILL_DIR/openbao-raft.snap.sha256 " '
)
encrypt_snapshot_command = (
' export RESTORE_DRILL_DIR= " $ { RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill} " \n '
f ' age -r { quoted_public_key } -o " $RESTORE_DRILL_DIR/openbao-raft.snap.age " " $RESTORE_DRILL_DIR/openbao-raft.snap " \n '
' sha256sum " $RESTORE_DRILL_DIR/openbao-raft.snap.age " | tee " $RESTORE_DRILL_DIR/openbao-raft.snap.age.sha256 " \n '
' if command -v shred >/dev/null 2>&1; then \n '
' shred -u " $RESTORE_DRILL_DIR/openbao-raft.snap " \n '
" else \n "
' rm -f " $RESTORE_DRILL_DIR/openbao-raft.snap " \n '
" fi "
)
isolated_restore_command = (
" cat << ' RESTORE_DRILL ' \n "
" Isolated Railiance OpenBao restore drill evidence required: \n "
" 1. Use a disposable cluster, VM, or namespace. Do not restore into namespace=openbao. \n "
" 2. Deploy a fresh OpenBao instance with empty storage. \n "
" 3. Decrypt the encrypted snapshot only inside the isolated drill workspace. \n "
" 4. Restore with: bao operator raft snapshot restore -force /tmp/openbao-raft.snap \n "
" 5. Unseal the isolated instance with the current trial/drill shares. \n "
" 6. Verify status, mounts, auth methods, policies, and a non-production test secret read. \n "
" 7. Destroy the isolated environment and record only non-secret evidence in this UI. \n "
" RESTORE_DRILL "
)
2026-05-25 13:38:03 +02:00
return [
2026-05-25 23:10:02 +02:00
action (
" OpenBao status " ,
" Show seal, initialization, storage, and HA state for the OpenBao pod. This command does not require a token. " ,
openbao_status_command ,
downstream_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Unseal by prompt " ,
" Provide threshold shares interactively. Never put shares on the command line. " ,
" kubectl exec -it -n openbao openbao-0 -- bao operator unseal " ,
direct_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" bao audit list " ,
" List OpenBao audit devices using a token entered by local hidden prompt. " ,
audit_list_command ,
downstream_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" bao secrets list " ,
" List enabled OpenBao secrets engines using a token entered by local hidden prompt. " ,
secrets_list_command ,
downstream_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" bao auth list " ,
" List enabled OpenBao auth methods using a token entered by local hidden prompt. " ,
auth_list_command ,
downstream_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-26 00:23:06 +02:00
action (
" Create platform-admin token " ,
" Create a renewable 24-hour non-root OpenBao token with the platform-admin policy. The emitted token is secret; store it immediately through the approved operator secret path. " ,
platform_admin_token_command ,
downstream_taint ,
) ,
2026-05-25 23:10:02 +02:00
action (
" Start unseal-key rotation " ,
2026-05-26 00:09:19 +02:00
" Run once to start a new 3-share, threshold-2 rotation. If rotation is already in progress, do not rerun init; check status and submit existing shares. " ,
2026-05-25 23:56:55 +02:00
rotate_init_command ,
2026-05-25 23:10:02 +02:00
compromise_taint ,
2026-05-25 14:57:53 +02:00
) ,
2026-05-26 00:09:19 +02:00
action (
" Check unseal-key rotation status " ,
" Inspect the active rotation without submitting a share. Use after init or when OpenBao says rotation is already in progress. " ,
rotate_status_command ,
compromise_taint ,
) ,
2026-05-25 23:10:02 +02:00
action (
" Submit current shares for rotation " ,
2026-05-26 00:09:19 +02:00
" Enter the nonce from rotation init, then token and existing unseal-share prompts. Repeat with distinct current shares until progress reaches the threshold. " ,
2026-05-25 23:56:55 +02:00
rotate_submit_command ,
2026-05-25 23:10:02 +02:00
compromise_taint ,
2026-05-25 18:31:48 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Cancel key rotation " ,
2026-05-25 23:56:55 +02:00
" Abort a started rotation if the nonce, share handling, or ceremony context is wrong. Requires a root/sudo-capable token. " ,
rotate_cancel_command ,
2026-05-25 23:10:02 +02:00
compromise_taint ,
2026-05-25 18:31:48 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Emergency seal OpenBao " ,
" Prompt locally for an OpenBao token and seal Railiance OpenBao without placing the token on the command line. " ,
seal_command ,
downstream_taint ,
2026-05-25 18:31:48 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Confirm sealed status " ,
" Check that Railiance OpenBao reports Sealed true after an emergency seal. " ,
openbao_status_command ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Prepare restore drill workspace " ,
" Create a local restricted directory for temporary snapshot evidence. " ,
snapshot_workspace_command ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Create encrypted-restore snapshot source " ,
" Prompt locally for an OpenBao token, create a Raft snapshot in the pod, copy it out, remove the pod copy, and record a plaintext hash before encryption. " ,
snapshot_command ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Encrypt restore snapshot " ,
" Encrypt the Raft snapshot to the custodian age recipient and remove the local plaintext snapshot. Replace <age-recipient> if no recipient is recorded. " ,
encrypt_snapshot_command ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Run isolated restore proof " ,
" Checklist for proving the snapshot can restore into an isolated OpenBao instance before live secrets move in. " ,
isolated_restore_command ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 23:10:02 +02:00
action (
" Run post-restore readiness check " ,
" Re-run the Railiance post-unseal checks after restore evidence has been captured. " ,
" make -C ../railiance-platform openbao-verify-post-unseal " ,
downstream_taint ,
2026-05-25 18:48:23 +02:00
) ,
2026-05-25 13:38:03 +02:00
]
2026-05-25 11:49:51 +02:00
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 )
2026-05-26 01:17:42 +02:00
admin_rows = admin_identity_payloads ( data )
2026-05-25 11:49:51 +02:00
artifact_rows = artifact_payloads ( data )
2026-05-25 23:36:45 +02:00
cleanup_done = yes ( data , " cleanup_complete " )
reopened = yes ( data , " platform_reopened " )
2026-05-25 11:49:51 +02:00
return [
{
2026-05-25 18:31:48 +02:00
" key " : " intro " ,
" name " : " Introduction & Actors " ,
" status " : " ok " ,
" reason " : " NetKingdom purpose, global actors, and supported custody shapes are visible. " ,
2026-05-25 11:49:51 +02:00
} ,
{
" key " : " subsystems " ,
2026-05-25 18:31:48 +02:00
" name " : " Subsystems & Scopes " ,
2026-05-25 11:49:51 +02:00
" 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. " ,
} ,
2026-05-25 18:31:48 +02:00
{
" 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. " ,
} ,
2026-05-25 11:49:51 +02:00
{
" 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. " ,
} ,
2026-05-26 01:17:42 +02:00
{
" key " : " admin-identity " ,
" name " : " Admin Identity Integration " ,
" status " : " ok " if all ( row [ " state " ] == " ok " for row in admin_rows ) else " set " ,
" reason " : " OpenBao admin access is bound to NetKingdom OIDC claims. " if all ( row [ " state " ] == " ok " for row in admin_rows ) else " Configure and verify the KeyCape-backed OpenBao admin path. " ,
} ,
2026-05-25 18:31:48 +02:00
{
" 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. " ,
} ,
2026-05-25 13:38:03 +02:00
{
" key " : " runbooks " ,
" name " : " Usecases & Runbooks " ,
2026-05-25 23:10:02 +02:00
" status " : " ok " ,
" reason " : " Reusable actions and runbook templates are available; execution state is tracked by Integration & Tests and explicit confirmations. " ,
2026-05-25 13:38:03 +02:00
} ,
2026-05-25 23:36:45 +02:00
{
" key " : " handover " ,
" name " : " Final Handover " ,
" status " : " ok " if reopened else " set " if cleanup_done else " err " ,
" reason " : (
" Platform is marked reopened under custody. "
if reopened
else " Cleanup is complete; confirm the platform has reopened under custody. "
if cleanup_done
else " Complete cleanup, taint response, and hardening before reopening. "
) ,
} ,
2026-05-25 11:49:51 +02:00
{
2026-05-25 18:31:48 +02:00
" key " : " terminology " ,
" name " : " Terminology & Patterns " ,
" status " : " ok " ,
" reason " : " Shared NetKingdom security terms and patterns are documented for operators. " ,
2026-05-25 11:49:51 +02:00
} ,
]
2026-05-24 09:26:02 +02:00
def status_payload ( data : dict [ str , Any ] , metadata_path : Path ) - > dict [ str , Any ] :
2026-05-24 17:04:15 +02:00
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 )
2026-05-25 13:13:47 +02:00
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 )
2026-05-24 09:26:02 +02:00
return {
" metadata_path " : str ( metadata_path ) ,
2026-05-24 17:04:15 +02:00
" stage " : derive_stage ( merged ) ,
2026-05-25 23:36:45 +02:00
" stage_steps " : stage_payloads ( merged ) ,
2026-05-25 13:38:03 +02:00
" next_action " : next_action ( gates , kit_validation ( merged ) , merged ) ,
2026-05-24 09:26:02 +02:00
" gates " : [ gate_payload ( gate ) for gate in gates ] ,
2026-05-24 17:04:15 +02:00
" key_custody_gates " : [ gate_payload ( gate ) for gate in key_custody_validation ( merged ) ] ,
" kit_gates " : [ gate_payload ( gate ) for gate in kit_validation ( merged ) ] ,
2026-05-25 11:49:51 +02:00
" section_gates " : section_gate_payloads ( merged ) ,
" roles " : role_payloads ( merged ) ,
" subsystems " : subsystem_payloads ( merged ) ,
" integrations " : integration_payloads ( merged ) ,
2026-05-26 01:17:42 +02:00
" admin_identity_integrations " : admin_identity_payloads ( merged ) ,
2026-05-25 13:38:03 +02:00
" runbooks " : runbook_payloads ( merged ) ,
2026-05-25 11:49:51 +02:00
" artifacts " : artifact_payloads ( merged ) ,
2026-05-25 13:13:47 +02:00
" commands " : command_payloads ( merged ) ,
2026-05-25 13:38:03 +02:00
" runbook_commands " : runbook_command_payloads ( merged ) ,
2026-05-24 17:04:15 +02:00
" bootstrap_secret_state " : bootstrap_secret_state ( ) ,
" metadata " : metadata_view ,
2026-05-24 09:26:02 +02:00
" approval_phrase " : APPROVAL_PHRASE ,
" custody_approval_modes " : sorted ( CUSTODY_APPROVAL_MODES ) ,
}
2026-05-24 17:04:15 +02:00
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 ( 760 px , 100 % ) ; margin : 0 auto ; padding : 28 px ; } }
section { { border : 1 px solid #111; border-radius: 6px; background: #fffdf7; padding: 20px; }}
h1 { { margin : 0 0 12 px ; font - size : 24 px ; } }
p { { line - height : 1.4 ; } }
table { { width : 100 % ; border - collapse : collapse ; margin : 16 px 0 ; background : #fff; }}
th , td { { border : 1 px solid #d8d3c7; padding: 9px; text-align: left; vertical-align: top; overflow-wrap: anywhere; }}
th { { width : 150 px ; } }
a { { display : inline - flex ; min - height : 38 px ; align - items : center ; justify - content : center ; border : 1 px 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 > """
2026-05-24 09:26:02 +02:00
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;
2026-05-25 14:57:53 +02:00
- - taint : #fde8e3;
- - taint - line : #ba6b61;
2026-05-24 09:26:02 +02:00
}
* { 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 : 1 px solid var ( - - line ) ;
background : var ( - - paper ) ;
padding : 22 px 28 px ;
}
. eyebrow {
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
letter - spacing : 0 ;
text - transform : uppercase ;
}
h1 {
margin : 8 px 0 0 ;
font - size : 28 px ;
line - height : 1.15 ;
font - weight : 650 ;
}
main {
width : min ( 1180 px , 100 % ) ;
margin : 0 auto ;
padding : 24 px ;
}
. topline {
display : grid ;
grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ;
border : 1 px solid var ( - - line ) ;
background : var ( - - paper ) ;
border - radius : 6 px ;
}
. metric {
min - width : 0 ;
padding : 16 px ;
border - right : 1 px solid var ( - - soft - line ) ;
}
. metric : last - child { border - right : 0 ; }
. label {
display : block ;
color : var ( - - muted ) ;
font - size : 13 px ;
margin - bottom : 6 px ;
}
. value {
display : block ;
font - size : 17 px ;
line - height : 1.25 ;
overflow - wrap : anywhere ;
}
2026-05-25 23:36:45 +02:00
. stage - rail {
display : grid ;
2026-05-26 01:17:42 +02:00
grid - template - columns : repeat ( 6 , minmax ( 0 , 1 fr ) ) ;
2026-05-25 23:36:45 +02:00
gap : 10 px ;
margin - top : 14 px ;
}
. stage - card {
min - width : 0 ;
border : 1 px solid var ( - - soft - line ) ;
border - radius : 6 px ;
background : var ( - - paper ) ;
padding : 12 px ;
}
. stage - card . done { background : var ( - - ok ) ; }
. stage - card . active {
border - color : var ( - - line ) ;
box - shadow : inset 0 0 0 2 px var ( - - hi ) ;
}
. stage - card . pending { background : #ffffff; }
. stage - id {
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
text - transform : uppercase ;
color : var ( - - muted ) ;
}
. stage - name {
margin - top : 5 px ;
font - weight : 650 ;
line - height : 1.2 ;
}
. stage - description , . stage - next {
margin - top : 6 px ;
color : var ( - - muted ) ;
font - size : 13 px ;
line - height : 1.35 ;
}
2026-05-24 09:26:02 +02:00
. layout {
display : grid ;
grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 320 px , 0.72 fr ) ;
gap : 18 px ;
margin - top : 18 px ;
align - items : start ;
}
. panel {
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
background : var ( - - paper ) ;
padding : 18 px ;
}
2026-05-25 18:31:48 +02:00
#approval-form {
display : flex ;
flex - direction : column ;
}
. workflow - section [ data - section = " intro " ] { order : 1 ; }
. workflow - section [ data - section = " subsystems " ] { order : 2 ; }
. workflow - section [ data - section = " roles " ] { order : 3 ; }
. workflow - section [ data - section = " integrations " ] { order : 4 ; }
2026-05-26 01:17:42 +02:00
. workflow - section [ data - section = " admin-identity " ] { order : 5 ; }
. workflow - section [ data - section = " artifacts " ] { order : 6 ; }
. workflow - section [ data - section = " runbooks " ] { order : 7 ; }
. workflow - section [ data - section = " handover " ] { order : 8 ; }
. workflow - section [ data - section = " terminology " ] { order : 9 ; }
. workflow - actions { order : 10 ; }
2026-05-24 09:26:02 +02:00
. panel + . panel { margin - top : 18 px ; }
h2 {
margin : 0 0 14 px ;
font - size : 18 px ;
line - height : 1.2 ;
}
. grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 14 px ;
}
. field { min - width : 0 ; }
input [ type = " text " ] , select {
width : 100 % ;
min - height : 42 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 4 px ;
background : var ( - - field ) ;
color : var ( - - ink ) ;
font : inherit ;
padding : 9 px 10 px ;
}
input [ type = " checkbox " ] , input [ type = " radio " ] {
accent - color : var ( - - ink ) ;
width : 16 px ;
height : 16 px ;
margin : 0 ;
}
. choice - list {
display : grid ;
gap : 8 px ;
}
. choice {
display : grid ;
grid - template - columns : 20 px minmax ( 0 , 1 fr ) ;
gap : 10 px ;
align - items : start ;
border : 1 px solid var ( - - soft - line ) ;
border - radius : 4 px ;
background : #ffffff;
padding : 10 px ;
}
. choice strong {
display : block ;
font - weight : 650 ;
line - height : 1.2 ;
}
2026-05-24 17:04:15 +02:00
. step - number {
display : inline - grid ;
place - items : center ;
width : 20 px ;
height : 20 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
background : var ( - - hi ) ;
}
2026-05-24 09:26:02 +02:00
. choice span {
color : var ( - - muted ) ;
display : block ;
font - size : 13 px ;
line - height : 1.3 ;
margin - top : 3 px ;
}
. notice {
border - left : 6 px solid var ( - - hi ) ;
background : #fff9d8;
padding : 12 px 14 px ;
line - height : 1.35 ;
margin : 0 0 16 px ;
}
2026-05-25 01:25:47 +02:00
. spec - list {
margin : 0 0 16 px ;
padding - left : 20 px ;
color : var ( - - muted ) ;
font - size : 13 px ;
line - height : 1.45 ;
}
. spec - list li { margin : 5 px 0 ; }
. system - note {
border : 1 px solid var ( - - soft - line ) ;
background : #ffffff;
padding : 12 px 14 px ;
line - height : 1.35 ;
margin : 0 0 16 px ;
}
. conditional { display : none ; }
. conditional . visible { display : grid ; }
2026-05-24 09:26:02 +02:00
. actions {
display : flex ;
flex - wrap : wrap ;
gap : 10 px ;
align - items : center ;
margin - top : 16 px ;
}
2026-05-24 17:04:15 +02:00
. inline - actions {
display : flex ;
flex - wrap : wrap ;
gap : 8 px ;
margin - top : 8 px ;
}
2026-05-24 09:26:02 +02:00
button {
min - height : 42 px ;
border : 1 px solid var ( - - ink ) ;
border - radius : 4 px ;
background : var ( - - ink ) ;
color : #ffffff;
font : inherit ;
font - weight : 650 ;
padding : 10 px 14 px ;
cursor : pointer ;
}
button . secondary {
background : var ( - - paper ) ;
color : var ( - - ink ) ;
}
2026-05-24 17:04:15 +02:00
. button - link {
display : inline - flex ;
align - items : center ;
justify - content : center ;
min - height : 36 px ;
border : 1 px solid var ( - - ink ) ;
border - radius : 4 px ;
background : var ( - - ink ) ;
color : #ffffff;
font - weight : 650 ;
padding : 7 px 11 px ;
text - decoration : none ;
}
. button - link . secondary {
background : var ( - - paper ) ;
color : var ( - - ink ) ;
}
2026-05-24 09:26:02 +02:00
button : disabled {
cursor : wait ;
opacity : 0.65 ;
}
2026-05-25 11:49:51 +02:00
summary {
display : flex ;
justify - content : space - between ;
gap : 12 px ;
align - items : center ;
cursor : pointer ;
list - style : none ;
}
summary : : - webkit - details - marker { display : none ; }
. summary - title {
font - size : 18 px ;
line - height : 1.2 ;
font - weight : 650 ;
}
. section - gate {
border : 1 px solid var ( - - soft - line ) ;
border - radius : 4 px ;
background : #ffffff;
padding : 10 px 12 px ;
margin : 14 px 0 ;
color : var ( - - muted ) ;
font - size : 13 px ;
line - height : 1.35 ;
}
. record - list , . command - list {
display : grid ;
gap : 8 px ;
margin : 14 px 0 ;
}
. record - row {
display : grid ;
2026-05-25 13:13:47 +02:00
grid - template - columns : 58 px minmax ( 170 px , 1.25 fr ) minmax ( 130 px , 0.72 fr ) minmax ( 150 px , 1 fr ) ;
2026-05-25 11:49:51 +02:00
gap : 10 px ;
align - items : start ;
border : 1 px solid var ( - - soft - line ) ;
border - radius : 4 px ;
background : #ffffff;
padding : 10 px ;
}
2026-05-25 14:57:53 +02:00
. record - row . tainted , . command - row . tainted {
background : var ( - - taint ) ;
border - color : var ( - - taint - line ) ;
}
2026-05-25 11:49:51 +02:00
. record - name {
font - weight : 650 ;
line - height : 1.2 ;
}
. record - description , . record - meta {
color : var ( - - muted ) ;
font - size : 13 px ;
line - height : 1.35 ;
margin - top : 3 px ;
overflow - wrap : anywhere ;
}
2026-05-25 14:57:53 +02:00
. taint - note {
border - left : 4 px solid var ( - - taint - line ) ;
color : var ( - - ink ) ;
font - size : 13 px ;
line - height : 1.35 ;
margin - top : 7 px ;
padding - left : 8 px ;
overflow - wrap : anywhere ;
}
2026-05-25 13:13:47 +02:00
. record - context {
display : grid ;
gap : 6 px ;
min - width : 0 ;
}
2026-05-25 11:49:51 +02:00
. state {
display : inline - flex ;
justify - content : center ;
min - width : 42 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
padding : 3 px 8 px ;
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
line - height : 1.2 ;
text - transform : uppercase ;
background : var ( - - warn ) ;
}
. state . ok { background : var ( - - ok ) ; }
2026-05-25 13:13:47 +02:00
. state . done { background : var ( - - ok ) ; }
2026-05-25 11:49:51 +02:00
. state . set { background : var ( - - human ) ; }
2026-05-25 13:13:47 +02:00
. state . redo { background : var ( - - human ) ; }
2026-05-25 11:49:51 +02:00
. state . nil { background : #ffffff; }
2026-05-25 13:13:47 +02:00
. state . todo { background : var ( - - warn ) ; }
2026-05-25 11:49:51 +02:00
. state . err { background : var ( - - bad ) ; }
2026-05-25 13:13:47 +02:00
. state . blocked { background : var ( - - bad ) ; }
2026-05-25 23:10:02 +02:00
. state . template { background : #f1eee5; }
2026-05-25 11:49:51 +02:00
. role - chip {
display : inline - flex ;
max - width : 100 % ;
border : 1 px solid var ( - - soft - line ) ;
border - radius : 999 px ;
padding : 3 px 8 px ;
background : #ffffff;
color : var ( - - ink ) ;
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
overflow - wrap : anywhere ;
}
. command - row {
border : 1 px solid var ( - - soft - line ) ;
border - radius : 4 px ;
background : #ffffff;
padding : 10 px ;
}
. command - head {
display : flex ;
justify - content : space - between ;
gap : 12 px ;
align - items : start ;
}
. command - code {
display : block ;
width : 100 % ;
margin - top : 8 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 4 px ;
background : #181818;
color : #ffffff;
padding : 10 px 12 px ;
overflow - x : auto ;
white - space : pre - wrap ;
overflow - wrap : anywhere ;
}
. copy - button {
min - height : 32 px ;
padding : 6 px 10 px ;
font - size : 13 px ;
}
2026-05-24 09:26:02 +02:00
. gates {
border : 1 px solid var ( - - soft - line ) ;
border - radius : 4 px ;
overflow : hidden ;
}
. gate {
display : grid ;
grid - template - columns : 92 px minmax ( 0 , 1 fr ) ;
gap : 12 px ;
padding : 11 px 12 px ;
border - top : 1 px solid var ( - - soft - line ) ;
background : #ffffff;
}
. gate : first - child { border - top : 0 ; }
. pill {
align - self : start ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
padding : 3 px 8 px ;
text - align : center ;
font - family : " IBM Plex Mono " , ui - monospace , monospace ;
font - size : 12 px ;
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 : 13 px ;
line - height : 1.35 ;
margin - top : 3 px ;
}
. message {
margin - top : 12 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 4 px ;
background : #ffffff;
padding : 12 px ;
min - height : 46 px ;
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 : 13 px ;
}
2026-05-24 17:04:15 +02:00
a {
color : var ( - - ink ) ;
text - decoration - thickness : 1 px ;
text - underline - offset : 3 px ;
}
2026-05-24 09:26:02 +02:00
@media ( max - width : 820 px ) {
header { padding : 18 px ; }
main { padding : 16 px ; }
2026-05-25 23:36:45 +02:00
. topline , . layout , . grid , . stage - rail { grid - template - columns : 1 fr ; }
2026-05-25 11:49:51 +02:00
. record - row { grid - template - columns : 1 fr ; }
2026-05-24 09:26:02 +02:00
. metric {
border - right : 0 ;
border - bottom : 1 px solid var ( - - soft - line ) ;
}
. metric : last - child { border - bottom : 0 ; }
}
< / style >
< / head >
< body >
< header >
< div class = " eyebrow " > NetKingdom control surface < / div >
2026-05-25 01:25:47 +02:00
< h1 > Guided security bootstrap < / h1 >
2026-05-24 09:26:02 +02:00
< / 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 >
2026-05-25 23:36:45 +02:00
< section class = " stage-rail " id = " stage-rail " aria - label = " Bootstrap stages " > < / section >
2026-05-24 09:26:02 +02:00
< div class = " layout " >
< form id = " approval-form " >
2026-05-25 18:31:48 +02:00
< details class = " panel workflow-section " data - section = " intro " open >
< summary > < span class = " summary-title " > 1. Introduction & Actors < / span > < span class = " state nil " data - section - state = " intro " > nil < / span > < / summary >
< div class = " section-gate " data - section - gate = " intro " > Loading introduction gate . < / div >
< p class = " notice " > NetKingdom is the operating frame for accountable digital sovereignty : identity , custody , secrets , approvals , recovery , and emergency control are made explicit before subsystems are trusted with live assets . < / p >
< div class = " record-list " >
< div class = " record-row " > < span class = " state set " > role < / span > < div > < div class = " record-name " > King / CEO < / div > < div class = " record-description " > Ultimate accountable authority for the kingdom , final custody intent , and existential risk decisions . < / div > < / div > < div class = " record-meta " > Global actor < / div > < div class = " record-meta " > Always present < / div > < / div >
< div class = " record-row " > < span class = " state set " > role < / span > < div > < div class = " record-name " > Guardian / CSO < / div > < div class = " record-description " > Owns security posture , emergency lock - down readiness , incident response , and custody - risk review . < / div > < / div > < div class = " record-meta " > Global actor < / div > < div class = " record-meta " > Required for quorum setups < / div > < / div >
< div class = " record-row " > < span class = " state set " > role < / span > < div > < div class = " record-name " > Armorer / CTO < / div > < div class = " record-description " > Owns technical implementation , platform architecture , automation , and operational correctness . < / div > < / div > < div class = " record-meta " > Global actor < / div > < div class = " record-meta " > Required for quorum setups < / div > < / div >
< div class = " record-row " > < span class = " state set " > role < / span > < div > < div class = " record-name " > Steward / COO < / div > < div class = " record-description " > Owns process reliability , onboarding , offboarding , runbook hygiene , and day - to - day operating cadence . < / div > < / div > < div class = " record-meta " > Global actor < / div > < div class = " record-meta " > Five - person setup < / div > < / div >
< div class = " record-row " > < span class = " state set " > role < / span > < div > < div class = " record-name " > Treasurer / CFO < / div > < div class = " record-description " > Owns financial authorization , asset inventory , economic risk , and continuity of valuable holdings . < / div > < / div > < div class = " record-meta " > Global actor < / div > < div class = " record-meta " > Five - person setup < / div > < / div >
< / div >
< div class = " choice-list " >
< div class = " choice " > < span class = " step-number " > A < / span > < span > < strong > One Person Kingdom < / strong > < span > King / CEO only . Best for early bootstrap and solo custody ; fast but has no human quorum . < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > B < / span > < span > < strong > Three Person Kingdom < / strong > < span > King / CEO plus two others . Decisions and access control target a two - of - three quorum . < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > C < / span > < span > < strong > Five Person Kingdom < / strong > < span > All global actors active . Decisions and access control target a three - of - five quorum . < / span > < / span > < / div >
< / div >
< / details >
2026-05-25 11:49:51 +02:00
< details class = " panel workflow-section " data - section = " roles " open >
2026-05-25 18:31:48 +02:00
< summary > < span class = " summary-title " > 3. Roles & Responsibilities < / span > < span class = " state nil " data - section - state = " roles " > nil < / span > < / summary >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-25 13:13:47 +02:00
< 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 >
2026-05-25 11:49:51 +02:00
< / details >
< details class = " panel workflow-section " data - section = " subsystems " open >
2026-05-25 18:31:48 +02:00
< summary > < span class = " summary-title " > 2. Subsystems & Scopes < / span > < span class = " state nil " data - section - state = " subsystems " > nil < / span > < / summary >
2026-05-25 11:49:51 +02:00
< 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 " >
2026-05-24 17:04:15 +02:00
< label class = " field " >
2026-05-25 11:49:51 +02:00
< span class = " label " > Bootstrap mode < / span >
< select id = " bootstrap_mode " title = " Trial mode documents the process; custody mode is for real handover material. " >
2026-05-24 17:04:15 +02:00
< option value = " trial " > Trial < / option >
< option value = " custody " > Custody < / option >
< / select >
< / label >
< label class = " field " >
2026-05-25 11:49:51 +02:00
< span class = " label " > Credential label < / span >
< input id = " credential_label " type = " text " autocomplete = " off " title = " Dedicated credential label, usually platform-root. " >
2026-05-24 17:04:15 +02:00
< / 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 >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-24 17:04:15 +02:00
< 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 >
2026-05-25 11:49:51 +02:00
< input id = " identity_account_home " type = " text " autocomplete = " off " value = " lldap " title = " Identity subsystem where the platform-root account lives. " >
2026-05-24 17:04:15 +02:00
< / label >
< label class = " field " >
< span class = " label " > Account reference < / span >
2026-05-25 11:49:51 +02:00
< input id = " identity_account_reference " type = " text " autocomplete = " off " placeholder = " platform-root@lldap " title = " Non-secret account reference. " >
2026-05-24 17:04:15 +02:00
< / label >
< label class = " field " >
< span class = " label " > Admin group < / span >
2026-05-25 11:49:51 +02:00
< input id = " identity_group_reference " type = " text " autocomplete = " off " value = " net-kingdom-admins " title = " LLDAP group used for current bootstrap admin access. " >
2026-05-24 09:26:02 +02:00
< / label >
< label class = " field " >
< span class = " label " > Second factor < / span >
2026-05-25 11:49:51 +02:00
< select id = " mfa_class " title = " Hardware token is optional policy, not a default requirement. " >
2026-05-24 09:26:02 +02:00
< option value = " " > Select < / option >
< option value = " totp " > TOTP < / option >
< option value = " webauthn " > WebAuthn < / option >
2026-05-25 01:25:47 +02:00
< option value = " hardware-token " > Hardware token ( policy only ) < / option >
2026-05-24 09:26:02 +02:00
< / select >
< / label >
< label class = " field " >
2026-05-25 11:49:51 +02:00
< span class = " label " > MFA enrollment source < / span >
< select id = " mfa_enrollment_source " title = " The real verifier owns the QR code or setup key. " >
2026-05-24 09:26:02 +02:00
< 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 " >
2026-05-25 11:49:51 +02:00
< 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. " >
2026-05-24 09:26:02 +02:00
< / label >
< / div >
< div class = " choice-list " style = " margin-top: 14px; " >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-24 09:26:02 +02:00
< 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 >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-24 09:26:02 +02:00
< / div >
2026-05-25 11:49:51 +02:00
< / details >
< details class = " panel workflow-section " data - section = " integrations " open >
2026-05-25 18:31:48 +02:00
< summary > < span class = " summary-title " > 4. Integration & Tests < / span > < span class = " state nil " data - section - state = " integrations " > nil < / span > < / summary >
2026-05-25 11:49:51 +02:00
< div class = " section-gate " data - section - gate = " integrations " > Loading integration gate . < / div >
2026-05-25 23:10:02 +02:00
< p class = " notice " > This section tracks stateful integration runbooks and gates . Status belongs to these tasks ; reusable command - only actions live below in Usecases & Runbooks . < / p >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-25 02:02:14 +02:00
< / div >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-25 02:02:14 +02:00
< 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 >
2026-05-25 13:38:03 +02:00
< 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 >
2026-05-25 02:02:14 +02:00
< 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 >
2026-05-25 14:30:57 +02:00
< label class = " choice " > < input id = " openbao_post_unseal_verified " type = " checkbox " > < span > < strong > Post - unseal verification passed < / strong > < span > Filesystem and post - unseal readiness checks completed without recording secret material . < / span > < / span > < / label >
2026-05-25 15:14:59 +02:00
< label class = " choice " > < input id = " openbao_initial_config_applied " type = " checkbox " > < span > < strong > Initial configuration applied < / strong > < span > OpenBao auth , mounts , and policies were applied ; audit may remain a declarative follow - up . < / span > < / span > < / label >
2026-05-25 02:02:14 +02:00
< 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 >
2026-05-24 09:26:02 +02:00
< / div >
2026-05-25 02:02:14 +02:00
< 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 >
2026-05-25 11:49:51 +02:00
< div id = " command-list " class = " command-list " > < / div >
< / details >
2026-05-26 01:17:42 +02:00
< details class = " panel workflow-section " data - section = " admin-identity " open >
< summary > < span class = " summary-title " > 5. Admin Identity Integration < / span > < span class = " state nil " data - section - state = " admin-identity " > nil < / span > < / summary >
< div class = " section-gate " data - section - gate = " admin-identity " > Loading admin identity gate . < / div >
< p class = " notice " > This stage replaces manually minted OpenBao admin tokens as the normal path . KeyCape remains the identity issuer ; OpenBao maps verified NetKingdom group claims to the existing platform - admin policy . < / p >
< div id = " admin-identity-records " class = " record-list " > < / div >
< ul class = " spec-list " style = " margin-top: 14px; " >
< li > Register a dedicated KeyCape client such as < code > openbao - admin < / code > with exact localhost OpenBao callback URIs and the < code > groups < / code > scope . < / li >
< li > Configure an OpenBao OIDC / JWT auth mount against < code > https : / / kc . coulomb . social < / code > and bind < code > net - kingdom - admins < / code > to < code > platform - admin < / code > . < / li >
< li > Verify < code > platform - root < / code > can complete MFA - backed OpenBao login before root - token disposition and cleanup . < / li >
< / ul >
< div class = " choice-list " >
< label class = " choice " > < input id = " openbao_oidc_client_registered " type = " checkbox " > < span > < strong > OpenBao client registered in KeyCape < / strong > < span > The dedicated client exists with exact redirect URIs and allowed OIDC scopes . < / span > < / span > < / label >
< label class = " choice " > < input id = " openbao_oidc_auth_configured " type = " checkbox " > < span > < strong > OpenBao OIDC auth configured < / strong > < span > OpenBao trusts KeyCape and maps NetKingdom admin claims to the platform - admin policy . < / span > < / span > < / label >
< label class = " choice " > < input id = " openbao_oidc_admin_login_verified " type = " checkbox " > < span > < strong > OIDC admin login verified < / strong > < span > platform - root can obtain OpenBao admin access through KeyCape MFA without using a manually minted token . < / span > < / span > < / label >
< / div >
< / details >
2026-05-25 11:49:51 +02:00
< details class = " panel workflow-section " data - section = " artifacts " open >
2026-05-26 01:17:42 +02:00
< summary > < span class = " summary-title " > 6. Artefacts & Locations < / span > < span class = " state nil " data - section - state = " artifacts " > nil < / span > < / summary >
2026-05-25 11:49:51 +02:00
< 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 >
2026-05-25 18:31:48 +02:00
< / details >
2026-05-25 23:10:02 +02:00
< details class = " panel workflow-section " data - section = " runbooks " open >
2026-05-26 01:17:42 +02:00
< summary > < span class = " summary-title " > 7. Usecases & Runbooks < / span > < span class = " state nil " data - section - state = " runbooks " > nil < / span > < / summary >
2026-05-25 23:10:02 +02:00
< div class = " section-gate " data - section - gate = " runbooks " > Loading runbook gate . < / div >
< p class = " notice " > This section contains reusable actions and runbook templates . Action cards are copyable command references without task status ; runbook task state belongs to Integration & Tests or to the explicit confirmation gates . < / 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 >
< label class = " choice " > < input id = " openbao_emergency_lockdown_drilled " type = " checkbox " > < span > < strong > Emergency lock - down drill recorded < / strong > < span > Railiance OpenBao was sealed and status - confirmed during a drill or real lock - down . No token or share is recorded here . < / span > < / span > < / label >
< / div >
< div id = " runbook-command-list " class = " command-list " > < / div >
< / details >
2026-05-25 23:36:45 +02:00
< details class = " panel workflow-section " data - section = " handover " open >
2026-05-26 01:17:42 +02:00
< summary > < span class = " summary-title " > 8. Final Handover < / span > < span class = " state nil " data - section - state = " handover " > nil < / span > < / summary >
2026-05-25 23:36:45 +02:00
< div class = " section-gate " data - section - gate = " handover " > Loading final handover gate . < / div >
< p class = " notice " > This is the line between trial / bootstrap and operating under custody . Mark these only after root - token disposition , restore proof , taint response , and cleanup have been handled outside this UI . < / p >
< ul class = " spec-list " >
< li > Cleanup means bootstrap - era passwords , service tokens , temporary admin paths , trial OpenBao material , and plaintext secret exposure have been rotated , retired , reset , or explicitly accepted as residual risk . < / li >
< li > Reopen means the platform is intentionally operated again under the selected custody strategy , with break - glass and restore paths known . < / li >
< / ul >
< div class = " choice-list " >
< label class = " choice " > < input id = " cleanup_complete " type = " checkbox " > < span > < strong > Cleanup and hardening complete < / strong > < span > Bootstrap - era credentials , databases , access paths , and tainted materials have been reviewed and handled . < / span > < / span > < / label >
< label class = " choice " > < input id = " platform_reopened " type = " checkbox " > < span > < strong > Platform reopened under custody < / strong > < span > The operator accepts that the platform is now running under the approved custody model . < / span > < / span > < / label >
< / div >
< / details >
2026-05-25 18:31:48 +02:00
< details class = " panel workflow-section " data - section = " terminology " open >
2026-05-26 01:17:42 +02:00
< summary > < span class = " summary-title " > 9. Terminology & Patterns < / span > < span class = " state nil " data - section - state = " terminology " > nil < / span > < / summary >
2026-05-25 18:31:48 +02:00
< div class = " section-gate " data - section - gate = " terminology " > Loading terminology gate . < / div >
< p class = " notice " > These terms apply across NetKingdom . Subsystems may have their own names , but the control surface keeps the cross - subsystem security pattern visible . < / p >
< div class = " record-list " >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Subsystem < / div > < div class = " record-description " > A bounded tool or service with its own admin surface , state , and security model . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > LLDAP , privacyIDEA , KeyCape , OpenBao < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Artefact < / div > < div class = " record-description " > A named credential , key , token , group , policy , packet , bundle , or evidence item with an owner and location . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Track name , subsystem , role , location , state < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Custody < / div > < div class = " record-description " > The human and technical arrangement that controls who can access or recover critical material . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Single king , two - of - three , three - of - five < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Quorum < / div > < div class = " record-description " > A threshold of actors or shares required before sensitive decisions or recovery actions can proceed . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > 2 of 3 , 3 of 5 < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Taint < / div > < div class = " record-description " > A warning that work is downstream of a compromised , exposed , or trial - only artefact . Taint informs ; it does not hard - block the operator . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Keeps source reference visible < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Break - glass < / div > < div class = " record-description " > Emergency material that can restore control but is not part of normal daily operation . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Retrieve , use , review , rotate if needed < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Seal and unseal < / div > < div class = " record-description " > For Railiance OpenBao , sealing stops access to stored OpenBao assets ; unsealing requires the configured threshold of unseal shares . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Emergency lock - down and reopening < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Root token vs unseal shares < / div > < div class = " record-description " > The Railiance OpenBao initial root token is a superuser API credential after unseal . Unseal shares control whether OpenBao can decrypt and serve requests at all . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Different layers , different custody < / div > < / div >
< div class = " record-row " > < span class = " state set " > term < / span > < div > < div class = " record-name " > Least privilege < / div > < div class = " record-description " > Daily access should use scoped credentials ; root , quorum , and break - glass paths remain exceptional and reviewable . < / div > < / div > < div class = " record-meta " > Pattern < / div > < div class = " record-meta " > Delegate , expire , audit < / div > < / div >
< / div >
< / details >
< section class = " panel workflow-actions " >
< h2 > Actions < / h2 >
2026-05-24 09:26:02 +02:00
< div class = " actions " >
2026-05-24 17:04:15 +02:00
< button class = " secondary " id = " save-button " type = " button " title = " Save the visible non-secret progress fields to local metadata. " > Save progress < / button >
2026-05-25 01:25:47 +02:00
< button id = " approve-button " type = " submit " title = " Approve the selected custody strategy only after all kit gates are satisfied. " > Approve selected strategy < / button >
2026-05-24 17:04:15 +02:00
< button class = " secondary " id = " refresh-button " type = " button " title = " Reload the local metadata and gate status from disk. " > Refresh < / button >
2026-05-24 09:26:02 +02:00
< / div >
< div id = " message " class = " message " role = " status " > Waiting for local approval . < / div >
2026-05-25 18:31:48 +02:00
< / section >
2026-05-24 09:26:02 +02:00
< / form >
< aside >
< section class = " panel " >
< h2 > Bootstrap gates < / h2 >
< div id = " gates " class = " gates " > < / div >
< / section >
2026-05-24 17:04:15 +02:00
< section class = " panel " >
< h2 > Key custody < / h2 >
< div id = " key-gates " class = " gates " > < / div >
< / section >
2026-05-24 09:26:02 +02:00
< section class = " panel " >
< h2 > Kit gates < / h2 >
< div id = " kit-gates " class = " gates " > < / div >
< / section >
< / aside >
< / div >
< / main >
< script >
const fields = [
2026-05-24 17:04:15 +02:00
" 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 " ,
2026-05-24 09:26:02 +02:00
" credential_label " ,
2026-05-24 17:04:15 +02:00
" identity_account_home " ,
" identity_account_reference " ,
" identity_account_created " ,
" identity_group_reference " ,
" identity_group_confirmed " ,
" password_safe_confirmed " ,
" oidc_login_verified " ,
2026-05-24 09:26:02 +02:00
" setup_operator " ,
" notification_contact " ,
2026-05-25 11:49:51 +02:00
" role_setup_operator_email " ,
" role_platform_custodian_email " ,
" role_identity_admin_email " ,
" role_openbao_operator_email " ,
" role_recovery_custodian_email " ,
" role_future_quorum_email " ,
2026-05-24 09:26:02 +02:00
" mfa_class " ,
" mfa_enrollment_source " ,
" mfa_enrollment_reference " ,
" mfa_enrolled_confirmed " ,
" recovery_confirmed " ,
2026-05-25 02:02:14 +02:00
" custody_packet_prepared " ,
" openbao_preflight_passed " ,
2026-05-25 13:38:03 +02:00
" openbao_init_output_produced " ,
2026-05-25 02:02:14 +02:00
" openbao_initialized " ,
2026-05-25 14:30:57 +02:00
" openbao_post_unseal_verified " ,
2026-05-25 15:14:59 +02:00
" openbao_initial_config_applied " ,
2026-05-25 13:38:03 +02:00
" openbao_trial_material_exposed " ,
" openbao_compromise_response_complete " ,
" openbao_unseal_keys_rotated " ,
2026-05-25 18:31:48 +02:00
" openbao_emergency_lockdown_drilled " ,
2026-05-26 01:17:42 +02:00
" openbao_oidc_client_registered " ,
" openbao_oidc_auth_configured " ,
" openbao_oidc_admin_login_verified " ,
2026-05-25 02:02:14 +02:00
" root_token_disposition " ,
2026-05-25 23:36:45 +02:00
" restore_drill_passed " ,
" cleanup_complete " ,
" platform_reopened "
2026-05-24 09:26:02 +02:00
] ;
2026-05-25 13:13:47 +02:00
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 = { } ;
2026-05-24 09:26:02 +02:00
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 ) ;
}
}
2026-05-25 23:36:45 +02:00
function renderStageRail ( stages ) {
const root = document . getElementById ( " stage-rail " ) ;
root . replaceChildren ( ) ;
for ( const stage of stages | | [ ] ) {
const card = document . createElement ( " div " ) ;
card . className = " stage-card " + stage . status ;
card . title = stage . next | | " " ;
const id = document . createElement ( " div " ) ;
id . className = " stage-id " ;
id . textContent = stage . id + " / " + stage . status ;
const name = document . createElement ( " div " ) ;
name . className = " stage-name " ;
name . textContent = stage . name ;
const description = document . createElement ( " div " ) ;
description . className = " stage-description " ;
description . textContent = stage . description ;
const next = document . createElement ( " div " ) ;
next . className = " stage-next " ;
next . textContent = stage . next ;
card . append ( id , name , description , next ) ;
root . append ( card ) ;
}
}
2026-05-25 11:49:51 +02:00
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 ;
}
2026-05-25 14:57:53 +02:00
function makeTaintNote ( item ) {
if ( ! item | | ! item . tainted ) return null ;
const note = document . createElement ( " div " ) ;
note . className = " taint-note " ;
const source = item . taint_source | | " upstream taint " ;
const reference = item . taint_reference ? " ( " + item . taint_reference + " ) " : " " ;
const reason = item . taint_reason ? " : " + item . taint_reason : " " ;
note . textContent = " Tainted from " + source + reference + reason ;
return note ;
}
2026-05-25 11:49:51 +02:00
function renderRecords ( target , rows ) {
const root = document . getElementById ( target ) ;
root . replaceChildren ( ) ;
for ( const record of rows | | [ ] ) {
const row = document . createElement ( " div " ) ;
2026-05-25 14:57:53 +02:00
row . className = " record-row " + ( record . tainted ? " tainted " : " " ) ;
2026-05-25 11:49:51 +02:00
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 ) ;
2026-05-25 14:57:53 +02:00
const taintNote = makeTaintNote ( record ) ;
if ( taintNote ) identity . append ( taintNote ) ;
2026-05-25 11:49:51 +02:00
2026-05-25 13:13:47 +02:00
const context = document . createElement ( " div " ) ;
context . className = " record-context " ;
2026-05-25 11:49:51 +02:00
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 ) ) ;
2026-05-25 13:13:47 +02:00
context . append ( subsystem , responsibility ) ;
2026-05-25 11:49:51 +02:00
const location = document . createElement ( " div " ) ;
location . className = " record-meta " ;
location . textContent = record . location ;
2026-05-25 13:13:47 +02:00
row . append ( identity , context , location ) ;
2026-05-25 11:49:51 +02:00
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 ;
}
}
}
2026-05-25 13:38:03 +02:00
function renderCommands ( target , commands ) {
const root = document . getElementById ( target ) ;
2026-05-25 11:49:51 +02:00
root . replaceChildren ( ) ;
for ( const item of commands | | [ ] ) {
const row = document . createElement ( " div " ) ;
2026-05-25 14:57:53 +02:00
row . className = " command-row " + ( item . tainted ? " tainted " : " " ) ;
2026-05-25 11:49:51 +02:00
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 ;
2026-05-25 23:10:02 +02:00
title . append ( name , description ) ;
if ( item . status_reason ) {
const statusReason = document . createElement ( " div " ) ;
statusReason . className = " record-description " ;
statusReason . textContent = item . status_reason ;
title . append ( statusReason ) ;
}
2026-05-25 14:57:53 +02:00
const taintNote = makeTaintNote ( item ) ;
if ( taintNote ) title . append ( taintNote ) ;
2026-05-25 11:49:51 +02:00
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 ;
2026-05-25 13:13:47 +02:00
const commandActions = document . createElement ( " div " ) ;
commandActions . className = " inline-actions " ;
2026-05-25 23:10:02 +02:00
if ( item . status ) commandActions . append ( makeStateBadge ( item . status ) ) ;
commandActions . append ( button ) ;
2026-05-25 13:13:47 +02:00
head . append ( title , commandActions ) ;
2026-05-25 11:49:51 +02:00
const code = document . createElement ( " code " ) ;
code . className = " command-code " ;
code . textContent = item . command ;
row . append ( head , code ) ;
root . append ( row ) ;
}
}
2026-05-24 09:26:02 +02:00
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 ) ;
} ) ;
2026-05-25 02:02:14 +02:00
document . querySelectorAll ( " [name= ' custody_mode ' ] " ) . forEach ( ( input ) = > {
input . checked = false ;
} ) ;
const mode = metadata . custody_mode | | " " ;
2026-05-24 09:26:02 +02:00
const selected = document . querySelector ( ` [ name = ' custody_mode ' ] [ value = ' $ {mode} ' ] ` ) ;
if ( selected ) selected . checked = true ;
2026-05-25 01:25:47 +02:00
syncConditionalHardware ( ) ;
}
2026-05-25 13:13:47 +02:00
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 " ;
}
2026-05-25 01:25:47 +02:00
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 ) ;
2026-05-24 09:26:02 +02:00
}
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 ;
2026-05-25 23:36:45 +02:00
renderStageRail ( data . stage_steps ) ;
2026-05-24 09:26:02 +02:00
renderGates ( " gates " , data . gates ) ;
2026-05-24 17:04:15 +02:00
renderGates ( " key-gates " , data . key_custody_gates ) ;
2026-05-24 09:26:02 +02:00
renderGates ( " kit-gates " , data . kit_gates ) ;
2026-05-25 11:49:51 +02:00
renderSectionGates ( data . section_gates ) ;
renderRecords ( " roles-records " , data . roles ) ;
renderRecords ( " subsystems-records " , data . subsystems ) ;
renderRecords ( " integrations-records " , data . integrations ) ;
2026-05-26 01:17:42 +02:00
renderRecords ( " admin-identity-records " , data . admin_identity_integrations ) ;
2026-05-25 13:38:03 +02:00
renderRecords ( " runbooks-records " , data . runbooks ) ;
2026-05-25 11:49:51 +02:00
renderRecords ( " artifacts-records " , data . artifacts ) ;
2026-05-25 13:38:03 +02:00
renderCommands ( " command-list " , data . commands ) ;
renderCommands ( " runbook-command-list " , data . runbook_commands ) ;
2026-05-25 13:13:47 +02:00
currentMetadata = data . metadata | | { } ;
fillForm ( currentMetadata ) ;
setResponsibilityDirty ( false ) ;
2026-05-24 09:26:02 +02:00
}
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 {
2026-05-24 17:04:15 +02:00
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 ,
2026-05-24 09:26:02 +02:00
credential_label : document . getElementById ( " credential_label " ) . value . trim ( ) ,
2026-05-24 17:04:15 +02:00
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 ,
2026-05-24 09:26:02 +02:00
setup_operator : document . getElementById ( " setup_operator " ) . value . trim ( ) ,
notification_contact : document . getElementById ( " notification_contact " ) . value . trim ( ) ,
2026-05-25 11:49:51 +02:00
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 ( ) ,
2026-05-24 09:26:02 +02:00
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 : " " ,
2026-05-25 02:02:14 +02:00
openbao_preflight_passed : document . getElementById ( " openbao_preflight_passed " ) . checked ,
2026-05-25 13:38:03 +02:00
openbao_init_output_produced : document . getElementById ( " openbao_init_output_produced " ) . checked ,
2026-05-25 02:02:14 +02:00
openbao_initialized : document . getElementById ( " openbao_initialized " ) . checked ,
2026-05-25 14:30:57 +02:00
openbao_post_unseal_verified : document . getElementById ( " openbao_post_unseal_verified " ) . checked ,
2026-05-25 15:14:59 +02:00
openbao_initial_config_applied : document . getElementById ( " openbao_initial_config_applied " ) . checked ,
2026-05-25 13:38:03 +02:00
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 ,
2026-05-25 18:31:48 +02:00
openbao_emergency_lockdown_drilled : document . getElementById ( " openbao_emergency_lockdown_drilled " ) . checked ,
2026-05-26 01:17:42 +02:00
openbao_oidc_client_registered : document . getElementById ( " openbao_oidc_client_registered " ) . checked ,
openbao_oidc_auth_configured : document . getElementById ( " openbao_oidc_auth_configured " ) . checked ,
openbao_oidc_admin_login_verified : document . getElementById ( " openbao_oidc_admin_login_verified " ) . checked ,
2026-05-25 02:02:14 +02:00
root_token_disposition : document . getElementById ( " root_token_disposition " ) . value ,
restore_drill_passed : document . getElementById ( " restore_drill_passed " ) . checked ,
2026-05-25 23:36:45 +02:00
cleanup_complete : document . getElementById ( " cleanup_complete " ) . checked ,
platform_reopened : document . getElementById ( " platform_reopened " ) . checked ,
2026-05-24 09:26:02 +02:00
approval_phrase : document . getElementById ( " approval_phrase " ) . value ,
approved_by : document . getElementById ( " setup_operator " ) . value . trim ( )
} ;
}
2026-05-25 01:25:47 +02:00
document . getElementById ( " mfa_class " ) . addEventListener ( " change " , syncConditionalHardware ) ;
2026-05-25 13:13:47 +02:00
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 ) ;
}
} ) ;
2026-05-25 13:38:03 +02:00
document . addEventListener ( " click " , async ( event ) = > {
2026-05-25 11:49:51 +02:00
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 " ) ;
}
} ) ;
2026-05-24 09:26:02 +02:00
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 ( ) ;
2026-05-25 01:25:47 +02:00
setMessage ( " Selected custody strategy approved. OpenBao init remains a separate human ceremony. " , " ok " ) ;
2026-05-24 09:26:02 +02:00
} catch ( error ) {
setMessage ( " Request failed: " + error . message , " error " ) ;
} finally {
button . disabled = false ;
}
} ) ;
2026-05-24 17:04:15 +02:00
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 ;
}
} ) ;
2026-05-24 09:26:02 +02:00
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 )
2026-05-24 17:04:15 +02:00
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 )
2026-05-24 09:26:02 +02:00
def do_GET ( self ) - > None :
2026-05-24 17:04:15 +02:00
parsed = urllib . parse . urlparse ( self . path )
if parsed . path == " / " or parsed . path == " /index.html " :
self . send_html ( HTTPStatus . OK , ui_html ( ) )
2026-05-24 09:26:02 +02:00
return
2026-05-24 17:04:15 +02:00
if parsed . path == " /api/status " :
2026-05-24 09:26:02 +02:00
data = load_metadata ( metadata_path )
if not data :
data = metadata_template ( )
self . send_json ( HTTPStatus . OK , status_payload ( data , metadata_path ) )
return
2026-05-24 17:04:15 +02:00
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
2026-05-24 09:26:02 +02:00
self . send_error ( HTTPStatus . NOT_FOUND . value )
def do_POST ( self ) - > None :
2026-05-24 17:04:15 +02:00
if self . path not in { " /api/approve-custody " , " /api/save-progress " } :
2026-05-24 09:26:02 +02:00
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 )
2026-05-24 17:04:15 +02:00
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
2026-05-24 09:26:02 +02:00
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 )
2026-05-25 01:25:47 +02:00
response [ " message " ] = " Selected custody strategy approved. "
2026-05-24 09:26:02 +02:00
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 ( ) )