diff --git a/tools/security-bootstrap-console/README.md b/tools/security-bootstrap-console/README.md index ce09349..5f77f89 100644 --- a/tools/security-bootstrap-console/README.md +++ b/tools/security-bootstrap-console/README.md @@ -76,6 +76,22 @@ python3 tools/security-bootstrap-console/security_bootstrap_console.py \ Open `http://127.0.0.1:8765`. +The web UI is structured as: + +1. **Roles & Responsibilities** - global bootstrap roles with designated + operator emails. +2. **Subsystems & Scope** - installation and initial access for LLDAP, + privacyIDEA, KeyCape, the custodian age envelope, and Railiance OpenBao. +3. **Integration & Tests** - OIDC and OpenBao preflight checks, with every + operator command shown as a copyable console block. +4. **Artefacts & Locations** - final non-secret overview of established + artefacts and where to find their custody references. + +Role, subsystem, integration, and artefact records use the same fields: +`name`, `description`, `subsystem`, `responsibility`, `location`, and `state`. +States are `nil`, `set`, `err`, and `ok`. Role chips expose the designated +email as hover text. + The UI is a guide and approval surface, not the identity provider. Current lightweight-mode credential placement is: diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index eea2fee..e11ec9f 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -537,6 +537,12 @@ def merged_approval_metadata( "custodian_age_private_key_reference", "setup_operator", "notification_contact", + "role_setup_operator_email", + "role_platform_custodian_email", + "role_identity_admin_email", + "role_openbao_operator_email", + "role_recovery_custodian_email", + "role_future_quorum_email", "mfa_class", "mfa_enrollment_source", "mfa_enrollment_reference", @@ -750,6 +756,12 @@ def metadata_template() -> dict[str, Any]: "identity_group_confirmed": False, "setup_operator": "tegwick", "notification_contact": "bernd.worsch@gmail.com", + "role_setup_operator_email": "bernd.worsch@gmail.com", + "role_platform_custodian_email": "bernd.worsch@gmail.com", + "role_identity_admin_email": "bernd.worsch@gmail.com", + "role_openbao_operator_email": "bernd.worsch@gmail.com", + "role_recovery_custodian_email": "bernd.worsch@gmail.com", + "role_future_quorum_email": "", "storage_classes": ["password-safe", "offline-packet"], "password_safe_confirmed": False, "mfa_class": "totp", @@ -812,6 +824,383 @@ def gate_payload(gate: Gate) -> dict[str, str]: } +def role_email(data: dict[str, Any], role_key: str) -> str: + fallback = str(data.get("notification_contact") or "").strip() + return str(data.get(role_key) or fallback).strip() + + +def role_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + rows = [ + ( + "setup-operator", + "Runs bootstrap commands and records non-secret evidence.", + "NetKingdom bootstrap", + "role_setup_operator_email", + ), + ( + "platform-root-custodian", + "Holds the dedicated platform-root credential and approves custody gates.", + "NetKingdom identity", + "role_platform_custodian_email", + ), + ( + "identity-admin", + "Administers LLDAP, privacyIDEA, and login repair during bootstrap.", + "Identity stack", + "role_identity_admin_email", + ), + ( + "openbao-ceremony-operator", + "Runs the attended OpenBao ceremony without copying secret output into the control surface.", + "Railiance OpenBao", + "role_openbao_operator_email", + ), + ( + "recovery-custodian", + "Can recover the platform-root credential and encrypted bootstrap bundle outside this UI.", + "Custody packet", + "role_recovery_custodian_email", + ), + ( + "future-quorum-custodian", + "Reserved for later two-of-three custody migration.", + "Custody strategy", + "role_future_quorum_email", + ), + ] + payloads: list[dict[str, str]] = [] + for name, description, subsystem, key in rows: + email = role_email(data, key) + payloads.append( + { + "name": name, + "description": description, + "subsystem": subsystem, + "responsibility": name, + "email": email, + "location": "Role assignment in local bootstrap metadata." if email else "Not assigned yet.", + "state": "set" if email else "nil", + } + ) + return payloads + + +def state_value(ok: bool, set_value: bool = False, err: bool = False) -> str: + if err: + return "err" + if ok: + return "ok" + if set_value: + return "set" + return "nil" + + +def subsystem_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + public_key = extract_age_public_key(data.get("custodian_age_public_key")) + state = bootstrap_secret_state() + return [ + { + "name": "age recipient", + "description": "Public age recipient used to encrypt bootstrap bundles.", + "subsystem": "custodian age envelope", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": age_public_key_fingerprint(public_key) or "No public recipient registered.", + "state": state_value( + bool(public_key) and yes(data, "custodian_age_public_key_confirmed"), + bool(public_key), + ), + }, + { + "name": "platform-root user", + "description": "Dedicated LLDAP account used as the current king credential.", + "subsystem": "LLDAP", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": str(data.get("identity_account_reference") or "Not recorded."), + "state": state_value(identity_account_ready(data), bool(data.get("identity_account_reference"))), + }, + { + "name": "platform-root MFA token", + "description": "Second factor enrolled with the authority that verifies login.", + "subsystem": "privacyIDEA", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": str(data.get("mfa_enrollment_reference") or "Not recorded."), + "state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")), + }, + { + "name": "bootstrap OIDC client", + "description": "KeyCape login path used to verify platform-root can authenticate with MFA.", + "subsystem": "KeyCape", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": KEYCAPE_ISSUER, + "state": state_value(identity_login_ready(data), bool(data.get("identity_account_reference"))), + }, + { + "name": "openbao-0", + "description": "Railiance OpenBao pod, services, PVCs, and sealed pre-init state.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "namespace=openbao, pod=openbao-0", + "state": state_value(yes(data, "openbao_preflight_passed"), yes(data, "custody_mode_approved")), + }, + { + "name": "encrypted bootstrap bundle", + "description": "Encrypted bootstrap secret bundle; plaintext directory must be absent.", + "subsystem": "sso-mfa/bootstrap", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": str(state["encrypted_bundle_path"]), + "state": state_value( + bool(state["encrypted_bundle_exists"]) and not bool(state["plaintext_secrets_present"]), + bool(state["encrypted_bundle_exists"]), + bool(state["plaintext_secrets_present"]), + ), + }, + ] + + +def integration_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + return [ + { + "name": "LLDAP admin group assignment", + "description": "platform-root is assigned to the current NetKingdom admin group.", + "subsystem": "LLDAP", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": str(data.get("identity_group_reference") or "Not recorded."), + "state": state_value(yes(data, "identity_group_confirmed"), bool(data.get("identity_group_reference"))), + }, + { + "name": "privacyIDEA MFA verification", + "description": "The same platform-root account has an enrolled second factor.", + "subsystem": "privacyIDEA", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": str(data.get("mfa_enrollment_reference") or "Not recorded."), + "state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")), + }, + { + "name": "KeyCape OIDC login", + "description": "platform-root completed the OIDC login check through KeyCape.", + "subsystem": "KeyCape", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": "local bootstrap callback", + "state": state_value(yes(data, "oidc_login_verified")), + }, + { + "name": "OpenBao preflight", + "description": "Railiance status and verify targets passed in the approved pre-init state.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "../railiance-platform", + "state": state_value(yes(data, "openbao_preflight_passed"), yes(data, "custody_mode_approved")), + }, + { + "name": "OpenBao init/unseal ceremony", + "description": "Attended ceremony creates unseal shares and initial root token outside this UI.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "operator shell with custody packet present", + "state": state_value(yes(data, "openbao_initialized"), yes(data, "openbao_preflight_passed")), + }, + ] + + +def artifact_payloads(data: dict[str, Any]) -> list[dict[str, str]]: + public_key = extract_age_public_key(data.get("custodian_age_public_key")) + state = bootstrap_secret_state() + root_disposition = str(data.get("root_token_disposition") or "") + return [ + { + "name": "platform-root", + "description": "Dedicated LLDAP user for the king credential.", + "subsystem": "LLDAP", + "responsibility": "platform-root-custodian", + "email": role_email(data, "role_platform_custodian_email"), + "location": str(data.get("identity_account_reference") or "Not recorded."), + "state": state_value(identity_account_ready(data), bool(data.get("identity_account_reference"))), + }, + { + "name": "net-kingdom-admins", + "description": "Current lightweight admin group for the platform-root identity.", + "subsystem": "LLDAP", + "responsibility": "identity-admin", + "email": role_email(data, "role_identity_admin_email"), + "location": str(data.get("identity_group_reference") or "Not recorded."), + "state": state_value(yes(data, "identity_group_confirmed"), bool(data.get("identity_group_reference"))), + }, + { + "name": "platform-root password entry", + "description": "Password-safe entry for the dedicated identity password. Value is never stored here.", + "subsystem": "password safe", + "responsibility": "platform-root-custodian", + "email": role_email(data, "role_platform_custodian_email"), + "location": "operator password safe / offline packet", + "state": state_value(yes(data, "password_safe_confirmed")), + }, + { + "name": "TOTP token", + "description": "privacyIDEA token for the platform-root login path.", + "subsystem": "privacyIDEA", + "responsibility": "platform-root-custodian", + "email": role_email(data, "role_platform_custodian_email"), + "location": str(data.get("mfa_enrollment_reference") or "Not recorded."), + "state": state_value(second_factor_ready(data), yes(data, "mfa_enrolled_confirmed")), + }, + { + "name": "age recipient", + "description": "Public recipient used for encrypted bootstrap bundles.", + "subsystem": "custodian age envelope", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": age_public_key_fingerprint(public_key) or "Not recorded.", + "state": state_value(bool(public_key) and yes(data, "custodian_age_public_key_confirmed"), bool(public_key)), + }, + { + "name": "age private key reference", + "description": "Non-secret pointer to the private age key location.", + "subsystem": "custodian age envelope", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": str(data.get("custodian_age_private_key_reference") or "Not recorded."), + "state": state_value(yes(data, "custodian_age_private_key_confirmed"), bool(data.get("custodian_age_private_key_reference"))), + }, + { + "name": "secrets.enc", + "description": "Encrypted bootstrap bundle.", + "subsystem": "sso-mfa/bootstrap", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": str(state["encrypted_bundle_path"]), + "state": state_value(bool(state["encrypted_bundle_exists"]), False, bool(state["plaintext_secrets_present"])), + }, + { + "name": "custody strategy", + "description": "Selected OpenBao ceremony control model.", + "subsystem": "custody model", + "responsibility": "platform-root-custodian", + "email": role_email(data, "role_platform_custodian_email"), + "location": str(data.get("custody_mode") or "Not selected."), + "state": state_value(yes(data, "custody_mode_approved"), data.get("custody_mode") in VALID_CUSTODY_MODES), + }, + { + "name": "recovery material", + "description": "Recovery references for identity, MFA, age key, and encrypted bootstrap bundle.", + "subsystem": "custody packet", + "responsibility": "recovery-custodian", + "email": role_email(data, "role_recovery_custodian_email"), + "location": "offline packet / password safe references", + "state": state_value(yes(data, "recovery_confirmed")), + }, + { + "name": "OpenBao custody packet", + "description": "Ceremony envelope with share assignment slots and root-token disposition plan.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "offline ceremony packet", + "state": state_value(yes(data, "custody_packet_prepared")), + }, + { + "name": "unseal shares", + "description": "Real OpenBao shares produced by init. They must be routed directly to approved custody locations.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": "created during attended init; not stored here", + "state": state_value(yes(data, "openbao_initialized"), yes(data, "openbao_preflight_passed")), + }, + { + "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"}, yes(data, "openbao_initialized")), + }, + ] + + +def command_payloads() -> list[dict[str, str]]: + return [ + { + "name": "OpenBao status", + "description": "Show pod, service, PVC, and seal/init status.", + "command": "make -C ../railiance-platform openbao-status", + }, + { + "name": "OpenBao preflight", + "description": "Run safe status and verification checks. Does not initialize OpenBao.", + "command": ( + "python3 tools/security-bootstrap-console/security_bootstrap_console.py " + "openbao-preflight --railiance-path ../railiance-platform --run" + ), + }, + { + "name": "OpenBao init ceremony", + "description": "Creates real unseal shares and the initial root token. Run once, attended.", + "command": "kubectl exec -n openbao openbao-0 -- bao operator init -key-shares=3 -key-threshold=2", + }, + { + "name": "OpenBao unseal prompt", + "description": "Enter unseal shares by prompt. Do not place shares on the command line.", + "command": "kubectl exec -n openbao openbao-0 -- bao operator unseal", + }, + { + "name": "OpenBao initial configuration", + "description": "Apply first audit, auth, mount, and policy configuration after unseal.", + "command": "make -C ../railiance-platform openbao-configure-initial", + }, + { + "name": "OpenBao post-unseal verification", + "description": "Verify filesystem and post-unseal readiness before live secrets move in.", + "command": "make -C ../railiance-platform openbao-verify-post-unseal", + }, + ] + + +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) + artifact_rows = artifact_payloads(data) + return [ + { + "key": "roles", + "name": "Roles & Responsibilities", + "status": "ok" if role_ok else "set", + "reason": "All active bootstrap roles have a designated email." if role_ok else "Assign an email to each active bootstrap role.", + }, + { + "key": "subsystems", + "name": "Subsystems & Scope", + "status": "ok" if all(row["state"] in {"ok", "set"} for row in subsystem_rows) else "err", + "reason": "Subsystems have install/access evidence." if all(row["state"] != "nil" for row in subsystem_rows) else "Complete subsystem setup fields and confirmations.", + }, + { + "key": "integrations", + "name": "Integration & Tests", + "status": "ok" if all(row["state"] == "ok" for row in integration_rows[:4]) else "set", + "reason": "Identity and OpenBao preflight checks are done." if all(row["state"] == "ok" for row in integration_rows[:4]) else "Run or confirm the remaining integration checks.", + }, + { + "key": "artifacts", + "name": "Artefacts & Locations", + "status": "ok" if all(row["state"] != "nil" for row in artifact_rows[:10]) else "set", + "reason": "Core custody artefacts have locations or confirmations." if all(row["state"] != "nil" for row in artifact_rows[:10]) else "Record missing artefact locations and confirmations.", + }, + ] + + def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]: merged = metadata_template() merged.update(data) @@ -827,6 +1216,12 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]: "gates": [gate_payload(gate) for gate in gates], "key_custody_gates": [gate_payload(gate) for gate in key_custody_validation(merged)], "kit_gates": [gate_payload(gate) for gate in kit_validation(merged)], + "section_gates": section_gate_payloads(merged), + "roles": role_payloads(merged), + "subsystems": subsystem_payloads(merged), + "integrations": integration_payloads(merged), + "artifacts": artifact_payloads(merged), + "commands": command_payloads(), "bootstrap_secret_state": bootstrap_secret_state(), "metadata": metadata_view, "approval_phrase": APPROVAL_PHRASE, @@ -1222,6 +1617,115 @@ def ui_html() -> str: cursor: wait; opacity: 0.65; } + summary { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + cursor: pointer; + list-style: none; + } + summary::-webkit-details-marker { display: none; } + .summary-title { + font-size: 18px; + line-height: 1.2; + font-weight: 650; + } + .section-gate { + border: 1px solid var(--soft-line); + border-radius: 4px; + background: #ffffff; + padding: 10px 12px; + margin: 14px 0; + color: var(--muted); + font-size: 13px; + line-height: 1.35; + } + .record-list, .command-list { + display: grid; + gap: 8px; + margin: 14px 0; + } + .record-row { + display: grid; + grid-template-columns: 62px minmax(170px, 1.2fr) minmax(120px, 0.7fr) minmax(150px, 0.8fr) minmax(150px, 1fr); + gap: 10px; + align-items: start; + border: 1px solid var(--soft-line); + border-radius: 4px; + background: #ffffff; + padding: 10px; + } + .record-name { + font-weight: 650; + line-height: 1.2; + } + .record-description, .record-meta { + color: var(--muted); + font-size: 13px; + line-height: 1.35; + margin-top: 3px; + overflow-wrap: anywhere; + } + .state { + display: inline-flex; + justify-content: center; + min-width: 42px; + border: 1px solid var(--line); + border-radius: 999px; + padding: 3px 8px; + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 12px; + line-height: 1.2; + text-transform: uppercase; + background: var(--warn); + } + .state.ok { background: var(--ok); } + .state.set { background: var(--human); } + .state.nil { background: #ffffff; } + .state.err { background: var(--bad); } + .role-chip { + display: inline-flex; + max-width: 100%; + border: 1px solid var(--soft-line); + border-radius: 999px; + padding: 3px 8px; + background: #ffffff; + color: var(--ink); + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 12px; + overflow-wrap: anywhere; + } + .command-row { + border: 1px solid var(--soft-line); + border-radius: 4px; + background: #ffffff; + padding: 10px; + } + .command-head { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: start; + } + .command-code { + display: block; + width: 100%; + margin-top: 8px; + border: 1px solid var(--line); + border-radius: 4px; + background: #181818; + color: #ffffff; + padding: 10px 12px; + overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + .copy-button { + min-height: 32px; + padding: 6px 10px; + font-size: 13px; + } .gates { border: 1px solid var(--soft-line); border-radius: 4px; @@ -1285,6 +1789,7 @@ def ui_html() -> str: header { padding: 18px; } main { padding: 16px; } .topline, .layout, .grid { grid-template-columns: 1fr; } + .record-row { grid-template-columns: 1fr; } .metric { border-right: 0; border-bottom: 1px solid var(--soft-line); @@ -1316,108 +1821,100 @@ def ui_html() -> str:
-
-

1. Bootstrap key envelope

-

The custodian age public key encrypts bootstrap bundles. The private key is ceremony material and must not be pasted into this UI.

-
-
1Register public recipientPaste only the custodian public age recipient, for example age1.... This value is safe to store and lets tools encrypt new bootstrap bundles.
-
2Record private-key custodyRecord a non-secret reference such as KeePassXC: custodian/age/private or offline USB label. The actual private key is provided only during an unlock/apply ceremony.
-
3Use trial before custodyTrial mode may use throwaway values to document the process. Custody mode encrypts real generated secrets immediately and shreds plaintext after apply.
-
-
+
+ 1. Roles & Responsibilitiesnil +
Loading role gate.
+

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.

+
+
+ + + + + + + +
+
+ +
+ 2. Subsystems & Scopenil +
Loading subsystem gate.
+

This section is about installing each subsystem and establishing initial user access. Integration checks come later.

+
+
+ + -
-
- - -
-
- -
-

2. Platform-root identity

-

Guide mode. OpenBao stores and audits secrets after the ceremony; it does not create the king account.

-
-
1Open LLDAP as bootstrap adminLLDAP has no public registration. Log in as admin using LLDAP_LDAP_USER_PASS from your password safe entry net-kingdom/LLDAP/admin. That value was generated during installation and injected into the lldap-secrets Kubernetes Secret.Open LLDAP
-
2Create dedicated accountCreate a dedicated platform-root or king user. Suggested values: username platform-root, notification contact bernd.worsch@gmail.com. Add it to net-kingdom-admins for the current lightweight path. Do not use tegwick as this account.
-
3Enroll MFAUse pi-admin only to confirm the LLDAP resolver, realm, and self-enrollment policy. Then log in to privacyIDEA self-service as the account recorded above, usually platform-root, 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.Open self-serviceOpen admin
-
4Confirm identity pathKeyCape 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.Start OIDC login checkOpen discoveryOpen health
-
5Then OpenBaoAfter custody approval, the OpenBao ceremony creates unseal shares, root-token disposition, policies, and temporary admin access.
-
-
-
-
- - - - -
-
- -
-

3. Credential record

-

Local non-secret metadata only. OpenBao initialization stays manual.

-
- - - -
-
- -
-

4. MFA and login proof

-

The QR code or setup key belongs to the authority that verifies login. This UI records confirmation only.

-
+ + + + +
-
- -
-

5. Custody strategy

-

Select the control strategy before preparing the custody packet. Approval comes later, after recovery material and packet contents match this strategy.

-
- - - + -
+ -
-

6. Recovery material

-

Recovery material is the ability to regain control of the platform-root credential and encrypted bootstrap bundle. It is not the OpenBao ceremony packet, and this UI stores only references.

-
    -
  • Platform-root password entry exists in the password safe and its label is known.
  • -
  • MFA recovery or re-enrollment path is known, such as privacyIDEA admin repair or a stored recovery-code location if that authority issues codes.
  • -
  • Custodian age private-key location is known and separate from the public recipient stored here.
  • -
  • Encrypted bootstrap bundle location is known; plaintext bootstrap secrets are absent before custody approval.
  • -
  • Notification contact and setup operator are recorded for lockout handling.
  • -
-
- - - - +
+ 3. Integration & Testsnil +
Loading integration gate.
+

This section connects the subsystems and shows every console command as a copyable block. Commands still run outside this browser so secret output never enters the control surface.

+
+ -
- -
-

7. Custody packet and approval

-

The custody packet is the offline ceremony envelope for the selected OpenBao strategy. It is prepared before approval, but it does not initialize OpenBao and does not contain secret values in this UI.

-
    -
  • Credential label, setup operator, notification contact, and selected custody strategy.
  • -
  • References to recovery material, not the recovery values themselves.
  • -
  • OpenBao init checklist, unseal-share assignment slots, and quorum plan.
  • -
  • Root-token disposition plan: revoke immediately or seal offline after scoped admin access works.
  • -
  • Signature/date line for the attended ceremony.
  • -
-
Secret capture is enforced by architecture: the control surface does not request secrets, and the gate checks local metadata plus plaintext bootstrap-secret presence. There is no user checkbox for this contract.
-
- -
-

Approval is the explicit handoff from preparation into OpenBao preflight. It still does not run OpenBao init.

-
- Approval phrase for selected strategy - -
-
- -
-

8. OpenBao setup path

-

OpenBao comes after custody approval. Public ingress is disabled in Railiance, so the attended ceremony uses Railiance commands, kubectl exec, or operator port-forwarding rather than this browser collecting secrets.

-
    -
  • Before approval, limit work to deployment/status checks: make -C ../railiance-platform openbao-dry-run, make -C ../railiance-platform openbao-deploy, and make -C ../railiance-platform openbao-status.
  • -
  • After approval, run safe preflight from this repo: python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight --railiance-path ../railiance-platform --run.
  • -
  • Human init ceremony on Railiance: kubectl exec -n openbao openbao-0 -- bao operator init -key-shares=3 -key-threshold=2, then distribute unseal shares according to the approved packet.
  • -
  • Unseal and configure without pasting values here: kubectl exec -n openbao openbao-0 -- bao operator unseal, then make -C ../railiance-platform openbao-configure-initial.
  • -
  • Verify and prove recovery: make -C ../railiance-platform openbao-verify-post-unseal, snapshot, and run an isolated restore drill before live secrets move in.
  • -
-
+
+ @@ -1506,13 +1965,42 @@ def ui_html() -> str: +
+ + +
+ 4. Artefacts & Locationsnil +
Loading artefact gate.
+

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.

+
+
+ + + +
+
    +
  • Recovery material: password recovery, MFA recovery/re-enrollment, custodian age-key recovery, encrypted bundle recovery, and notification contact references.
  • +
  • Custody packet: selected strategy, recovery references, OpenBao init checklist, unseal-share assignment slots, quorum plan, root-token disposition plan, and signature/date line.
  • +
+
+ + + + + +
+
Secret capture is enforced by architecture: the control surface does not request secrets, and the gate checks local metadata plus plaintext bootstrap-secret presence.
+
+ Approval phrase for selected strategy + +
Waiting for local approval.
-
+