diff --git a/docs/platform-root-custody.md b/docs/platform-root-custody.md index c605c7e..541b451 100644 --- a/docs/platform-root-custody.md +++ b/docs/platform-root-custody.md @@ -193,10 +193,13 @@ Before OpenBao initialization: 1. Use the guided bootstrap UX or checklist to decide the current trust stage. 2. Record `tegwick` as setup operator/contact, not as final root custodian. 3. Create or import the dedicated king credential and verify its second factor. -4. Prepare offline recovery bundle locations. -5. Choose whether this is temporary single-custodian king custody or preferred +4. Choose whether this is temporary single-custodian king custody or preferred independent escrow. -6. Run Railiance `make openbao-status` and `make openbao-verify`. +5. Prepare offline recovery bundle locations for that strategy. +6. Prepare the OpenBao custody packet for that strategy, including share + assignment rows, quorum plan, root-token disposition, and signoff line. +7. Approve the selected custody strategy in the NetKingdom control surface. +8. Run Railiance `make openbao-status` and `make openbao-verify`. During initialization: diff --git a/docs/security-bootstrap-openbao-ceremony-ux.md b/docs/security-bootstrap-openbao-ceremony-ux.md index 0f136ae..7c01948 100644 --- a/docs/security-bootstrap-openbao-ceremony-ux.md +++ b/docs/security-bootstrap-openbao-ceremony-ux.md @@ -30,7 +30,9 @@ Live initialization is blocked unless: - king credential kit is complete; - custody mode is selected; -- offline custody packet is prepared; +- recovery material is prepared for the selected custody mode; +- offline custody packet is prepared for the selected custody mode; +- selected custody mode is explicitly approved; - OpenBao pod and PVC preflight passes; - OpenBao reports `Initialized: false` and `Sealed: true`; - operator has acknowledged no secret output enters unsafe channels; diff --git a/tools/security-bootstrap-console/README.md b/tools/security-bootstrap-console/README.md index fd1722e..ce09349 100644 --- a/tools/security-bootstrap-console/README.md +++ b/tools/security-bootstrap-console/README.md @@ -57,6 +57,9 @@ notification/setup contact are all known. The custody packet is separate. It is the offline OpenBao ceremony envelope: selected custody strategy, recovery-material references, init checklist, unseal-share assignment slots, root-token disposition plan, and signature/date. +Select the custody strategy first, prepare recovery material and the custody +packet for that strategy, then approve the strategy. Only after that approval +should the OpenBao preflight/init sequence begin. Secret capture is an architecture gate, not a user checkbox. The control surface must not request or store passwords, OTP seeds, recovery codes, private @@ -164,6 +167,12 @@ python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-p This still does not run `bao operator init`. +OpenBao itself is operated from the Railiance runbook. Public ingress is +disabled, so the live ceremony uses Railiance `make` targets, `kubectl exec`, +or an operator port-forward. The local UI can record non-secret milestones +such as preflight passed, initialized/unsealed, root-token disposition, and +restore drill passed; it must never record root tokens or unseal shares. + Optional non-secret metadata can be supplied: ```bash diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index 0c37155..563f8a8 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -304,6 +304,11 @@ def kit_validation(data: dict[str, Any]) -> list[Gate]: "done" if identity_login_ready(data) else "blocked", "Verify the dedicated account can complete the NetKingdom login path.", ), + Gate( + "Custody strategy selected", + "done" if custody_mode in VALID_CUSTODY_MODES else "blocked", + "Choose how the OpenBao init ceremony will be controlled.", + ), Gate( "Recovery material", "done" if recovery_material_ready(data) else "blocked", @@ -319,18 +324,12 @@ def kit_validation(data: dict[str, Any]) -> list[Gate]: "done" if secret_boundary_ready(data) else "blocked", secret_boundary_reason(data), ), - Gate( - "Custody strategy selected", - "done" if custody_mode in VALID_CUSTODY_MODES else "blocked", - "Choose how the OpenBao init ceremony will be controlled.", - ), ] def king_kit_ready(data: dict[str, Any]) -> bool: gates = kit_validation(data) - required = [gate for gate in gates if gate.name != "Custody strategy selected"] - return all(gate.status == "done" for gate in required) + return all(gate.status == "done" for gate in gates) def custody_mode_approved(data: dict[str, Any]) -> bool: @@ -400,10 +399,32 @@ def build_gates(data: dict[str, Any]) -> list[Gate]: ] -def next_action(gates: list[Gate]) -> str: +def kit_next_action(kit_gates: list[Gate]) -> str: + labels = { + "Credential label": "Set credential label", + "Identity account": "Create platform-root account", + "Setup operator/contact": "Record setup contact", + "Storage class": "Select recovery storage", + "Password safe storage": "Confirm password safe entry", + "Second factor": "Enroll MFA factor", + "Identity login path": "Verify OIDC login", + "Custody strategy selected": "Select custody strategy", + "Recovery material": "Prepare recovery material", + "Custody packet": "Prepare custody packet", + "Control-surface secret boundary": "Confirm secret boundary", + } + for gate in kit_gates: + if gate.status != "done": + return labels.get(gate.name, "Define king credential kit") + return "Define king credential kit" + + +def next_action(gates: list[Gate], kit_gates: list[Gate] | None = None) -> str: for gate in gates: if gate.status == "blocked": if gate.name == "King credential kit": + if kit_gates is not None: + return kit_next_action(kit_gates) return "Define king credential kit" if gate.name == "Custody strategy approval": return "Approve custody strategy" @@ -423,6 +444,7 @@ def print_status(data: dict[str, Any]) -> None: merged.update(data) gates = build_gates(merged) key_gates = key_custody_validation(merged) + kit_gates = kit_validation(merged) state = bootstrap_secret_state() print("SECURITY BOOTSTRAP") print("") @@ -430,7 +452,7 @@ def print_status(data: dict[str, Any]) -> None: print(derive_stage(merged)) print("") print("Next safe action") - print(next_action(gates)) + print(next_action(gates, kit_gates)) print("") print("Key custody") public_key = extract_age_public_key(merged.get("custodian_age_public_key")) @@ -515,6 +537,7 @@ def merged_approval_metadata( "mfa_enrollment_source", "mfa_enrollment_reference", "custody_mode", + "root_token_disposition", "notes", ) for field in text_fields: @@ -536,6 +559,9 @@ def merged_approval_metadata( "identity_group_confirmed", "oidc_login_verified", "password_safe_confirmed", + "openbao_preflight_passed", + "openbao_initialized", + "restore_drill_passed", ): if field in payload: data[field] = payload[field] is True @@ -793,7 +819,7 @@ def status_payload(data: dict[str, Any], metadata_path: Path) -> dict[str, Any]: return { "metadata_path": str(metadata_path), "stage": derive_stage(merged), - "next_action": next_action(gates), + "next_action": next_action(gates, kit_validation(merged)), "gates": [gate_payload(gate) for gate in gates], "key_custody_gates": [gate_payload(gate) for gate in key_custody_validation(merged)], "kit_gates": [gate_payload(gate) for gate in kit_validation(merged)], @@ -1405,7 +1431,17 @@ def ui_html() -> str:
-

5. Recovery material

+

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.

-

6. Custody packet and approval

-

The custody packet is the offline ceremony envelope for OpenBao init. Recovery material proves access can be restored; the custody packet governs how the first real secrets are created, split, sealed, and signed off.

+

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.

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.
- -
-

Selecting a strategy records intent. Approval is a separate operator action that unlocks the next gate; it still does not run OpenBao init.

-
- - - +
+

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.

+ +
+ + + +
+
@@ -1494,7 +1550,11 @@ def ui_html() -> str: "mfa_enrollment_reference", "mfa_enrolled_confirmed", "recovery_confirmed", - "custody_packet_prepared" + "custody_packet_prepared", + "openbao_preflight_passed", + "openbao_initialized", + "root_token_disposition", + "restore_drill_passed" ]; function setMessage(text, kind) { @@ -1539,7 +1599,10 @@ def ui_html() -> str: document.querySelectorAll("[name='storage_classes']").forEach((input) => { input.checked = storage.includes(input.value); }); - const mode = metadata.custody_mode || "temporary-single-king"; + document.querySelectorAll("[name='custody_mode']").forEach((input) => { + input.checked = false; + }); + const mode = metadata.custody_mode || ""; const selected = document.querySelector(`[name='custody_mode'][value='${mode}']`); if (selected) selected.checked = true; syncConditionalHardware(); @@ -1594,6 +1657,10 @@ def ui_html() -> str: recovery_confirmed: document.getElementById("recovery_confirmed").checked, custody_packet_prepared: document.getElementById("custody_packet_prepared").checked, custody_mode: mode ? mode.value : "", + openbao_preflight_passed: document.getElementById("openbao_preflight_passed").checked, + openbao_initialized: document.getElementById("openbao_initialized").checked, + root_token_disposition: document.getElementById("root_token_disposition").value, + restore_drill_passed: document.getElementById("restore_drill_passed").checked, approval_phrase: document.getElementById("approval_phrase").value, approved_by: document.getElementById("setup_operator").value.trim() }; diff --git a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md index cc567c4..3ed7e10 100644 --- a/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md +++ b/workplans/NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md @@ -223,6 +223,13 @@ contents, distinguishes recovery material from the OpenBao custody packet, and turns "no secret capture" into an automatic control-surface boundary gate rather than a user checkbox. +**2026-05-25:** Corrected the custody/OpenBao ordering in the console: +strategy selection now comes before recovery/packet preparation, the custody +packet is prepared for the selected strategy before approval, and the OpenBao +panel now explains when to run Railiance preflight, init/unseal, +post-unseal configuration, root-token disposition, and restore proof. The +console still refuses to capture root tokens or unseal shares. + **2026-05-24:** Stepped back from ad hoc secret rollout and added the custodian age-key bootstrap model to the control surface. The UI now records the custodian public age recipient, a derived fingerprint, and a non-secret