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

@@ -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()
};