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
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 "
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 " )
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 " ,
" Select password-safe, offline-packet, hardware-token, or a combination. " ,
) ,
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-24 09:26:02 +02:00
Gate (
" Recovery material " ,
" done " if yes ( data , " recovery_confirmed " ) else " blocked " ,
" Confirm recovery material exists without recording values. " ,
) ,
Gate (
" Custody packet " ,
" done " if yes ( data , " custody_packet_prepared " ) else " blocked " ,
" Prepare the offline custody packet. " ,
) ,
Gate (
" No secret capture " ,
" done " if yes ( data , " no_secret_capture_confirmed " ) else " blocked " ,
" Confirm no secret values were stored in metadata, Git, State Hub, chat, tickets, or email. " ,
) ,
Gate (
" Custody mode " ,
" done " if custody_mode in VALID_CUSTODY_MODES else " blocked " ,
" Approve temporary-single-king, two-of-three-planned, or two-of-three-ready. " ,
) ,
]
def king_kit_ready ( data : dict [ str , Any ] ) - > bool :
gates = kit_validation ( data )
required = [ gate for gate in gates if gate . name != " Custody mode " ]
return all ( gate . status == " done " for gate in required )
def custody_mode_approved ( data : dict [ str , Any ] ) - > bool :
return data . get ( " custody_mode " ) in CUSTODY_APPROVAL_MODES and yes ( data , " custody_mode_approved " )
def custody_mode_reason ( data : dict [ str , Any ] ) - > str :
mode = data . get ( " custody_mode " )
if mode in CUSTODY_APPROVAL_MODES and yes ( data , " custody_mode_approved " ) :
return " Approved for the next gate under the selected custody mode. "
if mode == " two-of-three-planned " :
return " Two-of-three is recorded as the target, but live init stays blocked until it is ready. "
if mode in CUSTODY_APPROVAL_MODES and not yes ( data , " custody_mode_approved " ) :
return " Mode is selected but not yet explicitly approved. "
return " Choose temporary-single-king or two-of-three-ready for live OpenBao custody. "
def derive_stage ( data : dict [ str , Any ] ) - > str :
if yes ( data , " platform_reopened " ) :
return " S5 - Reopen under custody "
if yes ( data , " cleanup_complete " ) :
return " S4 - Cleanup and hardening "
if yes ( data , " openbao_initialized " ) :
return " S3 - OpenBao bootstrap "
if yes ( data , " king_credential_ready " ) or king_kit_ready ( data ) :
return " S2 - King credential preparation "
return DEFAULT_STAGE
def build_gates ( data : dict [ str , Any ] ) - > list [ Gate ] :
return [
Gate (
" King credential kit " ,
" done " if yes ( data , " king_credential_ready " ) or king_kit_ready ( data ) else " blocked " ,
" Dedicated king credential, second factor, and recovery storage. " ,
) ,
Gate (
" Custody mode " ,
" done " if custody_mode_approved ( data ) else " blocked " ,
custody_mode_reason ( data ) ,
) ,
Gate (
" OpenBao preflight " ,
" done " if yes ( data , " openbao_preflight_passed " ) else " blocked " ,
" Run safe Railiance OpenBao status and verification checks. " ,
) ,
Gate (
" OpenBao init ceremony " ,
" human " if not yes ( data , " openbao_initialized " ) else " done " ,
" Human-attended ceremony only. This console will not run init. " ,
) ,
Gate (
" Root-token disposition " ,
" done " if data . get ( " root_token_disposition " ) in { " revoked " , " offline-sealed " } else " blocked " ,
" Root token is revoked or sealed offline without recording value. " ,
) ,
Gate (
" Restore drill " ,
" done " if yes ( data , " restore_drill_passed " ) else " blocked " ,
" Snapshot and isolated restore proof before live secrets. " ,
) ,
Gate (
" Cleanup and rotation " ,
" done " if yes ( data , " cleanup_complete " ) else " blocked " ,
" Bootstrap-era credentials, databases, and access paths reviewed. " ,
) ,
]
def next_action ( gates : list [ Gate ] ) - > str :
for gate in gates :
if gate . status == " blocked " :
if gate . name == " King credential kit " :
return " Define king credential kit "
if gate . name == " Custody mode " :
return " Choose custody mode "
if gate . name == " OpenBao preflight " :
return " Run OpenBao preflight "
if gate . name == " Root-token disposition " :
return " Record root-token disposition "
if gate . name == " Restore drill " :
return " Run restore drill "
if gate . name == " Cleanup and rotation " :
return " Complete handover cleanup "
return " Review related workplans "
def print_status ( data : dict [ str , Any ] ) - > None :
2026-05-24 17:04:15 +02:00
merged = metadata_template ( )
merged . update ( data )
gates = build_gates ( merged )
key_gates = key_custody_validation ( merged )
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 " )
print ( next_action ( gates ) )
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 " ,
" mfa_class " ,
" mfa_enrollment_source " ,
" mfa_enrollment_reference " ,
" custody_mode " ,
" notes " ,
)
for field in text_fields :
if field in payload and payload [ field ] is not None :
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-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 :
errors . append ( f ' Type " { APPROVAL_PHRASE } " to approve custody mode. ' )
if mode not in VALID_CUSTODY_MODES :
errors . append ( " Select a custody mode. " )
elif mode not in CUSTODY_APPROVAL_MODES :
errors . append (
" two-of-three-planned is a target state, not live-init approval. "
" Use temporary-single-king now or two-of-three-ready when shares exist. "
)
for gate in kit_validation ( data ) :
if gate . name == " Custody mode " :
continue
if gate . status != " done " :
errors . append ( f " { gate . name } : { gate . reason } " )
return errors
def approve_custody_metadata (
existing : dict [ str , Any ] ,
payload : dict [ str , Any ] ,
approval_phrase : str ,
approver : str ,
) - > tuple [ dict [ str , Any ] , list [ str ] ] :
data = merged_approval_metadata ( existing , payload )
errors = validate_custody_approval ( data , approval_phrase )
if errors :
return data , errors
data [ " king_credential_ready " ] = True
data [ " custody_mode_approved " ] = True
data [ " custody_approved_at " ] = utc_now ( )
data [ " custody_approved_by " ] = approver or data . get ( " setup_operator " , " " )
data [ " approval_scope " ] = " Non-secret local custody-mode approval only. Does not run OpenBao init. "
return data , [ ]
def print_approve_custody_mode ( args : argparse . Namespace , data : dict [ str , Any ] ) - > int :
if args . metadata is None :
print ( " ERROR: approve-custody-mode requires --metadata /path/to/non-secret.json " , file = sys . stderr )
return 2
approval_phrase = args . approval_phrase or " "
if args . yes :
approval_phrase = APPROVAL_PHRASE
elif not approval_phrase :
print ( " This writes non-secret custody approval metadata only. " )
print ( " It will not run OpenBao init and will not store secret values. " )
try :
approval_phrase = input ( f ' Type " { APPROVAL_PHRASE } " to continue: ' )
except EOFError :
approval_phrase = " "
payload : dict [ str , Any ] = {
" custody_mode " : args . mode ,
}
for key in (
" credential_label " ,
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 " ,
" 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 ,
" openbao_initialized " : False ,
" root_token_disposition " : " " ,
" restore_drill_passed " : False ,
" cleanup_complete " : False ,
" platform_reopened " : False ,
" review_date " : " " ,
" notes " : " Non-secret metadata only. " ,
}
def print_openbao_preflight ( args : argparse . Namespace ) - > int :
print ( " OPENBAO PREFLIGHT " )
print ( " " )
print ( " Safe commands: " )
print ( f " make -C { args . railiance_path } openbao-status " )
print ( f " make -C { args . railiance_path } openbao-verify " )
print ( " " )
if not args . run :
print ( " Dry run only. Pass --run to execute safe preflight commands. " )
return 0
railiance_path = Path ( args . railiance_path ) . expanduser ( ) . resolve ( )
if not railiance_path . is_dir ( ) :
print ( f " ERROR: Railiance path not found: { railiance_path } " , file = sys . stderr )
return 2
for target in ( " openbao-status " , " openbao-verify " ) :
result = subprocess . run (
[ " make " , " -C " , str ( railiance_path ) , target ] ,
check = False ,
)
if result . returncode != 0 :
return result . returncode
return 0
def gate_payload ( gate : Gate ) - > dict [ str , str ] :
return {
" name " : gate . name ,
" status " : gate . status ,
" reason " : gate . reason ,
}
def status_payload ( data : dict [ str , Any ] , metadata_path : Path ) - > dict [ str , Any ] :
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-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-24 09:26:02 +02:00
" next_action " : next_action ( gates ) ,
" 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 ) ] ,
" 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;
}
* { 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 ;
}
. 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 ;
}
. 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 ;
}
. 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 ;
}
. 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 ; }
. topline , . layout , . grid { grid - template - columns : 1 fr ; }
. 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 >
< h1 > Security bootstrap custody approval < / h1 >
< / header >
< main >
< section class = " topline " aria - label = " Bootstrap status " >
< div class = " metric " >
< span class = " label " > Stage < / span >
< span class = " value " id = " stage " > Loading < / span >
< / div >
< div class = " metric " >
< span class = " label " > Next safe action < / span >
< span class = " value " id = " next-action " > Loading < / span >
< / div >
< div class = " metric " >
< span class = " label " > Metadata < / span >
< span class = " value " > < code id = " metadata-path " > Loading < / code > < / span >
< / div >
< / section >
< div class = " layout " >
< form id = " approval-form " >
2026-05-24 17:04:15 +02:00
< section class = " panel " >
< h2 > Bootstrap key custody < / h2 >
< p class = " notice " > The custodian age public key encrypts bootstrap bundles . The private key is ceremony material and must not be pasted into this UI . < / p >
< div class = " choice-list " >
< div class = " choice " > < span class = " step-number " > 1 < / span > < span > < strong > Register public recipient < / strong > < span > Paste only the custodian public age recipient , for example < code > age1 . . . < / code > . This value is safe to store and lets tools encrypt new bootstrap bundles . < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 2 < / span > < span > < strong > Record private - key custody < / strong > < span > Record a non - secret reference such as < code > KeePassXC : custodian / age / private < / code > or < code > offline USB label < / code > . The actual private key is provided only during an unlock / apply ceremony . < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 3 < / span > < span > < strong > Use trial before custody < / strong > < span > Trial mode may use throwaway values to document the process . Custody mode encrypts real generated secrets immediately and shreds plaintext after apply . < / span > < / span > < / div >
< / div >
< div class = " grid " style = " margin-top: 14px; " >
< label class = " field " >
< span class = " label " > Mode < / span >
< select id = " bootstrap_mode " >
< option value = " trial " > Trial < / option >
< option value = " custody " > Custody < / option >
< / select >
< / label >
< label class = " field " >
< span class = " label " > Public key fingerprint < / span >
< input id = " custodian_age_public_key_fingerprint " type = " text " readonly title = " Derived from the public age recipient; useful for comparing with your password safe entry. " >
< / label >
< label class = " field " >
< span class = " label " > Custodian public age key < / span >
< input id = " custodian_age_public_key " type = " text " autocomplete = " off " placeholder = " age1... " title = " Public recipient only. Never paste AGE-SECRET-KEY material here. " >
< / label >
< label class = " field " >
< span class = " label " > 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 >
< / div >
< div class = " choice-list " style = " margin-top: 14px; " >
< label class = " choice " > < input id = " custodian_age_public_key_confirmed " type = " checkbox " > < span > < strong > Public key confirmed < / strong > < span > The public recipient matches the custodian key material you intend to use . < / span > < / span > < / label >
< label class = " choice " > < input id = " custodian_age_private_key_confirmed " type = " checkbox " > < span > < strong > Private key location confirmed < / strong > < span > You know where the private key is stored and can unlock it intentionally later . < / span > < / span > < / label >
< / div >
< / section >
< section class = " panel " >
< h2 > Credential home < / h2 >
< p class = " notice " > Guide mode . OpenBao stores and audits secrets after the ceremony ; it does not create the king account . < / p >
< div class = " choice-list " >
< div class = " choice " > < span class = " step-number " > 1 < / span > < span > < strong > Open LLDAP as bootstrap admin < / strong > < span > LLDAP has no public registration . Log in as < code > admin < / code > using < code > LLDAP_LDAP_USER_PASS < / code > from your password safe entry < code > net - kingdom / LLDAP / admin < / code > . That value was generated during installation and injected into the < code > lldap - secrets < / code > Kubernetes Secret . < / span > < span 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 > < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 2 < / span > < span > < strong > Create dedicated account < / strong > < span > Create a dedicated < code > platform - root < / code > or < code > king < / code > user . Suggested values : username < code > platform - root < / code > , notification contact < code > bernd . worsch @gmail.com < / code > . Add it to < code > net - kingdom - admins < / code > for the current lightweight path . Do not use < code > tegwick < / code > as this account . < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 3 < / span > < span > < strong > Enroll MFA < / strong > < span > Use < code > pi - admin < / code > only to confirm the LLDAP resolver , realm , and self - enrollment policy . Then log in to privacyIDEA self - service as the account recorded above , usually < code > platform - root < / code > , for the QR code or setup key . If self - service is not ready , use admin - assisted token assignment as a fallback and record that as the enrollment source . < / span > < span class = " inline-actions " > < a class = " button-link " 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. Use pi-admin here only to check resolver, realm, policy, or fallback token assignment. " > Open admin < / a > < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 4 < / span > < span > < strong > Confirm identity path < / strong > < span > KeyCape is an OIDC issuer , not a dashboard ; its root path returning 404 is expected . The login check starts the dedicated bootstrap - console OIDC client and should return to this console . If it never reaches the callback page , KeyCape may still need the public Authelia redirect config , this callback URI registration , or a browser OTP prompt . Mark OIDC verified only after the browser flow works for the same account . < / span > < span class = " inline-actions " > < a class = " button-link " href = " /oidc/start " target = " _blank " rel = " noreferrer " title = " Start the bootstrap-console OIDC authorization flow through KeyCape. It should return to this local console callback without storing tokens. " > 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. This proves the issuer metadata is published. " > Open discovery < / a > < a class = " button-link secondary " href = " https://kc.coulomb.social/healthz " target = " _blank " rel = " noreferrer " title = " Open the KeyCape health endpoint. This proves the service process responds. " > Open health < / a > < / span > < / span > < / div >
< div class = " choice " > < span class = " step-number " > 5 < / span > < span > < strong > Then OpenBao < / strong > < span > After custody approval , the OpenBao ceremony creates unseal shares , root - token disposition , policies , and temporary admin access . < / span > < / span > < / div >
< / div >
< div class = " grid " style = " margin-top: 14px; " >
< label class = " field " >
< span class = " label " > Account home < / span >
< input id = " identity_account_home " type = " text " autocomplete = " off " value = " lldap " >
< / label >
< label class = " field " >
< span class = " label " > Account reference < / span >
< input id = " identity_account_reference " type = " text " autocomplete = " off " placeholder = " platform-root@lldap " >
< / label >
< label class = " field " >
< span class = " label " > Admin group < / span >
< input id = " identity_group_reference " type = " text " autocomplete = " off " value = " net-kingdom-admins " >
< / label >
< / div >
< div class = " choice-list " style = " margin-top: 14px; " >
< 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 . No value is stored here . < / span > < / span > < / label >
< 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 >
< / div >
< / section >
2026-05-24 09:26:02 +02:00
< section class = " panel " >
< h2 > King credential < / h2 >
< p class = " notice " > Local non - secret metadata only . OpenBao initialization stays manual . < / p >
< div class = " grid " >
< label class = " field " >
< span class = " label " > Credential label < / span >
< input id = " credential_label " type = " text " autocomplete = " off " >
< / label >
< label class = " field " >
< span class = " label " > Setup operator < / span >
< input id = " setup_operator " type = " text " autocomplete = " off " >
< / label >
< label class = " field " >
< span class = " label " > Notification contact < / span >
< input id = " notification_contact " type = " text " autocomplete = " off " >
< / label >
< label class = " field " >
< span class = " label " > Second factor < / span >
< select id = " mfa_class " >
< option value = " " > Select < / option >
< option value = " totp " > TOTP < / option >
< option value = " webauthn " > WebAuthn < / option >
< option value = " hardware-token " > Hardware token < / option >
< / select >
< / label >
< / div >
< / section >
< section class = " panel " >
< h2 > Second factor enrollment < / h2 >
< p class = " notice " > The QR code or setup key belongs to the authority that verifies login . This UI records confirmation only . < / p >
< div class = " grid " >
< label class = " field " >
< span class = " label " > Enrollment source < / span >
< select id = " mfa_enrollment_source " >
< option value = " deferred " > Deferred < / option >
< option value = " identity-provider " > Identity provider < / option >
< option value = " external-verifier " > External verifier < / option >
< option value = " hardware-registration " > Hardware registration < / option >
< / select >
< / label >
< label class = " field " >
< span class = " label " > Reference < / span >
< input id = " mfa_enrollment_reference " type = " text " autocomplete = " off " placeholder = " provider or vault entry label " >
< / label >
< / div >
< div class = " choice-list " style = " margin-top: 14px; " >
< label class = " choice " > < input id = " mfa_enrolled_confirmed " type = " checkbox " > < span > < strong > Factor enrolled < / strong > < span > The real verifier produced or accepted the factor . No seed is recorded here . < / span > < / span > < / label >
< / div >
< / section >
< section class = " panel " >
< h2 > Storage and recovery < / h2 >
< div class = " choice-list " >
< label class = " choice " > < input name = " storage_classes " value = " password-safe " type = " checkbox " > < span > < strong > Password safe < / strong > < span > Credential held in a dedicated vault entry . < / span > < / span > < / label >
< label class = " choice " > < input name = " storage_classes " value = " offline-packet " type = " checkbox " > < span > < strong > Offline packet < / strong > < span > Recovery material exists outside live systems . < / span > < / span > < / label >
< label class = " choice " > < input name = " storage_classes " value = " hardware-token " type = " checkbox " > < span > < strong > Hardware token < / strong > < span > Custody includes hardware - backed access . < / span > < / span > < / label >
< label class = " choice " > < input id = " recovery_confirmed " type = " checkbox " > < span > < strong > Recovery material confirmed < / strong > < span > No values recorded here . < / span > < / span > < / label >
< label class = " choice " > < input id = " custody_packet_prepared " type = " checkbox " > < span > < strong > Custody packet prepared < / strong > < span > Offline packet is ready for the ceremony . < / span > < / span > < / label >
< label class = " choice " > < input id = " no_secret_capture_confirmed " type = " checkbox " > < span > < strong > No secret capture < / strong > < span > No secrets in Git , State Hub , chat , tickets , email , or screenshots . < / span > < / span > < / label >
< / div >
< / section >
< section class = " panel " >
< h2 > Custody mode < / h2 >
< div class = " choice-list " >
< label class = " choice " > < input name = " custody_mode " value = " temporary-single-king " type = " radio " > < span > < strong > Temporary single king < / strong > < span > Recommended while Railiance is still pre - production . < / span > < / span > < / label >
< label class = " choice " > < input name = " custody_mode " value = " two-of-three-ready " type = " radio " > < span > < strong > Two of three ready < / strong > < span > Use when independent shares already exist . < / span > < / span > < / label >
< label class = " choice " > < input name = " custody_mode " value = " two-of-three-planned " type = " radio " > < span > < strong > Two of three planned < / strong > < span > Records intent but does not approve live init . < / span > < / span > < / label >
< / div >
< div class = " field " style = " margin-top: 14px; " >
< span class = " label " > Approval phrase < / span >
< input id = " approval_phrase " type = " text " autocomplete = " off " placeholder = " approve custody mode " >
< / div >
< div class = " actions " >
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 >
< button id = " approve-button " type = " submit " title = " Approve the selected custody mode only after all kit gates are satisfied. " > Approve custody mode < / button >
< 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 >
< / section >
< / 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 " ,
" mfa_class " ,
" mfa_enrollment_source " ,
" mfa_enrollment_reference " ,
" mfa_enrolled_confirmed " ,
" recovery_confirmed " ,
" custody_packet_prepared " ,
" no_secret_capture_confirmed "
] ;
function setMessage ( text , kind ) {
const element = document . getElementById ( " message " ) ;
element . textContent = text ;
element . className = " message " + ( kind ? " " + kind : " " ) ;
}
function renderGates ( target , gates ) {
const root = document . getElementById ( target ) ;
root . replaceChildren ( ) ;
for ( const gate of gates ) {
const row = document . createElement ( " div " ) ;
row . className = " gate " + gate . status ;
const status = document . createElement ( " div " ) ;
status . className = " pill " ;
status . textContent = gate . status ;
const body = document . createElement ( " div " ) ;
const title = document . createElement ( " div " ) ;
title . className = " gate-title " ;
title . textContent = gate . name ;
const reason = document . createElement ( " div " ) ;
reason . className = " gate-reason " ;
reason . textContent = gate . reason ;
body . append ( title , reason ) ;
row . append ( status , body ) ;
root . append ( row ) ;
}
}
function fillForm ( metadata ) {
for ( const id of fields ) {
const element = document . getElementById ( id ) ;
if ( ! element ) continue ;
if ( element . type == = " checkbox " ) {
element . checked = metadata [ id ] == = true ;
} else {
element . value = metadata [ id ] | | " " ;
}
}
const storage = metadata . storage_classes | | [ ] ;
document . querySelectorAll ( " [name= ' storage_classes ' ] " ) . forEach ( ( input ) = > {
input . checked = storage . includes ( input . value ) ;
} ) ;
const mode = metadata . custody_mode | | " temporary-single-king " ;
const selected = document . querySelector ( ` [ name = ' custody_mode ' ] [ value = ' $ {mode} ' ] ` ) ;
if ( selected ) selected . checked = true ;
}
async function loadStatus ( ) {
const response = await fetch ( " /api/status " ) ;
const data = await response . json ( ) ;
document . getElementById ( " stage " ) . textContent = data . stage ;
document . getElementById ( " next-action " ) . textContent = data . next_action ;
document . getElementById ( " metadata-path " ) . textContent = data . metadata_path ;
renderGates ( " gates " , data . gates ) ;
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 ) ;
fillForm ( data . metadata | | { } ) ;
}
function approvalPayload ( ) {
const storage = Array . from ( document . querySelectorAll ( " [name= ' storage_classes ' ]:checked " ) )
. map ( ( input ) = > input . value ) ;
const mode = document . querySelector ( " [name= ' custody_mode ' ]:checked " ) ;
return {
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 ( ) ,
mfa_class : document . getElementById ( " mfa_class " ) . value ,
mfa_enrollment_source : document . getElementById ( " mfa_enrollment_source " ) . value ,
mfa_enrollment_reference : document . getElementById ( " mfa_enrollment_reference " ) . value . trim ( ) ,
mfa_enrolled_confirmed : document . getElementById ( " mfa_enrolled_confirmed " ) . checked ,
storage_classes : storage ,
recovery_confirmed : document . getElementById ( " recovery_confirmed " ) . checked ,
custody_packet_prepared : document . getElementById ( " custody_packet_prepared " ) . checked ,
no_secret_capture_confirmed : document . getElementById ( " no_secret_capture_confirmed " ) . checked ,
custody_mode : mode ? mode . value : " " ,
approval_phrase : document . getElementById ( " approval_phrase " ) . value ,
approved_by : document . getElementById ( " setup_operator " ) . value . trim ( )
} ;
}
document . getElementById ( " approval-form " ) . addEventListener ( " submit " , async ( event ) = > {
event . preventDefault ( ) ;
const button = document . getElementById ( " approve-button " ) ;
button . disabled = true ;
try {
const response = await fetch ( " /api/approve-custody " , {
method : " POST " ,
headers : { " Content-Type " : " application/json " } ,
body : JSON . stringify ( approvalPayload ( ) )
} ) ;
const result = await response . json ( ) ;
if ( ! response . ok ) {
setMessage ( " Not approved: \\ n " + ( result . errors | | [ ] ) . map ( ( item ) = > " - " + item ) . join ( " \\ n " ) , " error " ) ;
renderGates ( " kit-gates " , result . kit_gates | | [ ] ) ;
return ;
}
document . getElementById ( " approval_phrase " ) . value = " " ;
await loadStatus ( ) ;
setMessage ( " Custody mode approved. OpenBao init remains a separate human ceremony. " , " ok " ) ;
} catch ( error ) {
setMessage ( " Request failed: " + error . message , " error " ) ;
} finally {
button . disabled = false ;
}
} ) ;
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 )
response [ " message " ] = " Custody mode approved. "
self . send_json ( HTTPStatus . OK , response )
return SecurityBootstrapUIHandler
def serve_web_ui ( args : argparse . Namespace ) - > int :
metadata_path = args . metadata or DEFAULT_METADATA_PATH
handler = make_ui_handler ( metadata_path )
httpd = ThreadingHTTPServer ( ( args . host , args . port ) , handler )
host = html . escape ( args . host )
print ( " SECURITY BOOTSTRAP UI " )
print ( " " )
print ( f " URL: http:// { host } : { args . port } " )
print ( f " Metadata: { metadata_path } " )
print ( " " )
print ( " Local non-secret custody approval only. Press Ctrl+C to stop. " )
try :
httpd . serve_forever ( )
except KeyboardInterrupt :
print ( " " )
print ( " Stopped. " )
finally :
httpd . server_close ( )
return 0
def refuse_live_init ( ) - > int :
print ( " REFUSED " )
print ( " " )
print ( " This console does not run bao operator init. " )
print ( " Use the human-attended Railiance OpenBao ceremony after all gates pass. " )
return 2
def build_parser ( ) - > argparse . ArgumentParser :
parser = argparse . ArgumentParser (
description = " Non-secret NetKingdom security bootstrap console. " ,
)
parser . add_argument (
" --metadata " ,
type = Path ,
help = " Optional non-secret metadata JSON file. " ,
)
sub = parser . add_subparsers ( dest = " command " , required = True )
sub . add_parser ( " status " , help = " Show trust stage, gates, and next safe action. " )
sub . add_parser ( " king-kit " , help = " Print king credential kit checklist. " )
sub . add_parser ( " validate-king-kit " , help = " Validate non-secret king credential metadata. " )
approve = sub . add_parser ( " approve-custody-mode " , help = " Approve a live-init-ready custody mode. " )
approve . add_argument (
" --mode " ,
choices = sorted ( VALID_CUSTODY_MODES ) ,
default = " temporary-single-king " ,
help = " Custody mode to record. two-of-three-planned cannot approve live init. " ,
)
approve . add_argument ( " --credential-label " , dest = " credential_label " )
approve . add_argument ( " --setup-operator " , dest = " setup_operator " )
approve . add_argument ( " --notification-contact " , dest = " notification_contact " )
approve . add_argument ( " --storage-class " , dest = " storage_class " , action = " append " )
approve . add_argument ( " --mfa-class " , dest = " mfa_class " , choices = sorted ( VALID_MFA_CLASSES ) )
approve . add_argument ( " --mfa-enrolled-confirmed " , action = " store_true " )
approve . add_argument (
" --mfa-enrollment-source " ,
choices = sorted ( VALID_MFA_ENROLLMENT_SOURCES ) ,
default = None ,
)
approve . add_argument ( " --mfa-enrollment-reference " )
approve . add_argument ( " --recovery-confirmed " , action = " store_true " )
approve . add_argument ( " --custody-packet-prepared " , action = " store_true " )
approve . add_argument ( " --no-secret-capture-confirmed " , action = " store_true " )
approve . add_argument ( " --approval-phrase " , default = " " )
approve . add_argument ( " --approved-by " , default = " " )
approve . add_argument ( " --notes " )
approve . add_argument (
" --yes " ,
action = " store_true " ,
help = f ' Use the required approval phrase " { APPROVAL_PHRASE } " non-interactively. ' ,
)
sub . add_parser ( " custody-packet " , help = " Print blank offline custody packet template. " )
sub . add_parser ( " handover-checklist " , help = " Print handover and cleanup checklist. " )
sub . add_parser ( " metadata-template " , help = " Print non-secret metadata JSON template. " )
sub . add_parser ( " refuse-live-init " , help = " Explain why live OpenBao init is refused. " )
web = sub . add_parser ( " web-ui " , help = " Serve a local custody approval UI. " )
web . add_argument ( " --host " , default = " 127.0.0.1 " , help = " Bind host. Defaults to localhost. " )
web . add_argument ( " --port " , type = int , default = 8765 , help = " Bind port. Defaults to 8765. " )
preflight = sub . add_parser ( " openbao-preflight " , help = " Show or run safe OpenBao preflight. " )
preflight . add_argument (
" --railiance-path " ,
default = " ../railiance-platform " ,
help = " Path to railiance-platform repo. " ,
)
preflight . add_argument (
" --run " ,
action = " store_true " ,
help = " Run safe preflight targets. Does not run OpenBao init. " ,
)
return parser
def main ( argv : list [ str ] | None = None ) - > int :
parser = build_parser ( )
args = parser . parse_args ( argv )
data = load_metadata ( args . metadata )
if args . command == " status " :
print_status ( data )
return 0
if args . command == " king-kit " :
print_king_kit ( )
return 0
if args . command == " validate-king-kit " :
return print_validate_king_kit ( data )
if args . command == " approve-custody-mode " :
return print_approve_custody_mode ( args , data )
if args . command == " custody-packet " :
print_custody_packet ( )
return 0
if args . command == " handover-checklist " :
print_handover_checklist ( )
return 0
if args . command == " metadata-template " :
print ( json . dumps ( metadata_template ( ) , indent = 2 ) )
return 0
if args . command == " openbao-preflight " :
return print_openbao_preflight ( args )
if args . command == " web-ui " :
return serve_web_ui ( args )
if args . command == " refuse-live-init " :
return refuse_live_init ( )
parser . error ( f " unknown command: { args . command } " )
return 2
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )