Guide OpenBao custody ceremony order

This commit is contained in:
2026-05-25 02:02:14 +02:00
parent 83cf2111c1
commit e45dd4f9eb
5 changed files with 115 additions and 27 deletions

View File

@@ -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:

View File

@@ -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;

View File

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

View File

@@ -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:
</section>
<section class="panel">
<h2>5. Recovery material</h2>
<h2>5. Custody strategy</h2>
<p class="notice">Select the control strategy before preparing the custody packet. Approval comes later, after recovery material and packet contents match this strategy.</p>
<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. One dedicated platform-root custodian controls the first ceremony, with migration to quorum custody planned.</span></span></label>
<label class="choice"><input name="custody_mode" value="two-of-three-ready" type="radio"><span><strong>Two of three ready</strong><span>Use only when independent share holders already exist and can attend the first OpenBao init ceremony.</span></span></label>
<label class="choice"><input name="custody_mode" value="two-of-three-planned" type="radio"><span><strong>Two of three planned</strong><span>Records the target model but does not approve live init yet.</span></span></label>
</div>
</section>
<section class="panel">
<h2>6. Recovery material</h2>
<p class="notice">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.</p>
<ul class="spec-list">
<li>Platform-root password entry exists in the password safe and its label is known.</li>
@@ -1423,8 +1459,8 @@ def ui_html() -> str:
</section>
<section class="panel">
<h2>6. Custody packet and approval</h2>
<p class="notice">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.</p>
<h2>7. Custody packet and approval</h2>
<p class="notice">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.</p>
<ul class="spec-list">
<li>Credential label, setup operator, notification contact, and selected custody strategy.</li>
<li>References to recovery material, not the recovery values themselves.</li>
@@ -1434,18 +1470,38 @@ def ui_html() -> str:
</ul>
<div class="system-note">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.</div>
<div class="choice-list" style="margin-bottom: 14px;">
<label class="choice"><input id="custody_packet_prepared" type="checkbox"><span><strong>Custody packet prepared</strong><span>The offline ceremony packet above is ready. No OpenBao root token or unseal share is recorded here.</span></span></label>
</div>
<p class="notice">Selecting a strategy records intent. Approval is a separate operator action that unlocks the next gate; it still does not run OpenBao init.</p>
<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>
<label class="choice"><input id="custody_packet_prepared" type="checkbox"><span><strong>Custody packet prepared</strong><span>The offline ceremony packet above is ready for the selected strategy. No OpenBao root token or unseal share is recorded here.</span></span></label>
</div>
<p class="notice">Approval is the explicit handoff from preparation into OpenBao preflight. It still does not run OpenBao init.</p>
<div class="field" style="margin-top: 14px;">
<span class="label">Approval phrase for selected strategy</span>
<input id="approval_phrase" type="text" autocomplete="off" placeholder="approve custody mode">
<input id="approval_phrase" type="text" autocomplete="off" placeholder="approve custody mode" title="Type the approval phrase only after the selected strategy, recovery material, and custody packet are ready.">
</div>
</section>
<section class="panel">
<h2>8. OpenBao setup path</h2>
<p class="notice">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.</p>
<ul class="spec-list">
<li>Before approval, limit work to deployment/status checks: <code>make -C ../railiance-platform openbao-dry-run</code>, <code>make -C ../railiance-platform openbao-deploy</code>, and <code>make -C ../railiance-platform openbao-status</code>.</li>
<li>After approval, run safe preflight from this repo: <code>python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight --railiance-path ../railiance-platform --run</code>.</li>
<li>Human init ceremony on Railiance: <code>kubectl exec -n openbao openbao-0 -- bao operator init -key-shares=3 -key-threshold=2</code>, then distribute unseal shares according to the approved packet.</li>
<li>Unseal and configure without pasting values here: <code>kubectl exec -n openbao openbao-0 -- bao operator unseal</code>, then <code>make -C ../railiance-platform openbao-configure-initial</code>.</li>
<li>Verify and prove recovery: <code>make -C ../railiance-platform openbao-verify-post-unseal</code>, snapshot, and run an isolated restore drill before live secrets move in.</li>
</ul>
<div class="choice-list">
<label class="choice"><input id="openbao_preflight_passed" type="checkbox"><span><strong>OpenBao preflight passed</strong><span>Status and verification checks completed after custody approval.</span></span></label>
<label class="choice"><input id="openbao_initialized" type="checkbox"><span><strong>Initialized and unsealed</strong><span>The human ceremony completed outside this UI under the approved strategy.</span></span></label>
<label class="choice"><input id="restore_drill_passed" type="checkbox"><span><strong>Restore drill passed</strong><span>Snapshot and isolated restore proof completed before live secrets are migrated.</span></span></label>
</div>
<label class="field" style="margin-top: 14px;">
<span class="label">Root-token disposition</span>
<select id="root_token_disposition" title="Record only what happened to the root token; never record the token value.">
<option value="">Not recorded</option>
<option value="revoked">Revoked after scoped admin works</option>
<option value="offline-sealed">Sealed offline</option>
</select>
</label>
<div class="actions">
<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 strategy only after all kit gates are satisfied.">Approve selected strategy</button>
@@ -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()
};

View File

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