From 82d69e006f182bdf1dbfa33537c69c034abab42f Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 25 May 2026 18:48:23 +0200 Subject: [PATCH] Add OpenBao restore drill actions --- .../security_bootstrap_console.py | 136 ++++++++++++++++++ ...-custody-and-openbao-identity-bootstrap.md | 6 + 2 files changed, 142 insertions(+) diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index 181eeed..9cd82ec 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -13,6 +13,7 @@ import base64 import hashlib import html import json +import shlex import subprocess import sys import urllib.error @@ -1338,6 +1339,8 @@ def command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: init_output = yes(data, "openbao_init_output_produced") initialized = yes(data, "openbao_initialized") + initial_config_applied = yes(data, "openbao_initial_config_applied") + restore_done = yes(data, "restore_drill_passed") trial_exposed = yes(data, "openbao_trial_material_exposed") response_complete = yes(data, "openbao_compromise_response_complete") keys_rotated = yes(data, "openbao_unseal_keys_rotated") @@ -1366,6 +1369,12 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: lockdown_status = "blocked" lockdown_location = "OpenBao is not recorded as unsealed; sealing is only meaningful while it is serving requests." + restore_status = "done" if restore_done else "todo" + restore_location = "Create, encrypt, and isolated-restore a Railiance OpenBao Raft snapshot before live secrets move in." + if not initial_config_applied: + restore_status = "blocked" + restore_location = "Apply OpenBao initial configuration before proving backup and restore." + return [ add_taint( { @@ -1403,12 +1412,26 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: }, openbao_downstream_taint if initialized else {}, ), + add_taint( + { + "name": "Restore drill", + "description": "Prove that Railiance OpenBao can be snapshotted, restored into isolation, unsealed, and verified before production trust.", + "subsystem": "Railiance OpenBao", + "responsibility": "openbao-ceremony-operator", + "email": role_email(data, "role_openbao_operator_email"), + "location": restore_location, + "state": restore_status, + }, + openbao_downstream_taint if initialized else {}, + ), ] def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: init_output = yes(data, "openbao_init_output_produced") initialized = yes(data, "openbao_initialized") + initial_config_applied = yes(data, "openbao_initial_config_applied") + restore_done = yes(data, "restore_drill_passed") trial_exposed = yes(data, "openbao_trial_material_exposed") response_complete = yes(data, "openbao_compromise_response_complete") keys_rotated = yes(data, "openbao_unseal_keys_rotated") @@ -1454,6 +1477,59 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: "unset OPENBAO_TOKEN" ) + restore_status = "done" if restore_done else "todo" + restore_reason = "Restore drill is recorded." if restore_done else "Create encrypted snapshot evidence and complete an isolated restore proof." + if not initial_config_applied: + restore_status = "blocked" + restore_reason = "OpenBao initial configuration must be applied before the restore drill." + restore_taint = openbao_downstream_taint if initialized else {} + public_key = extract_age_public_key(data.get("custodian_age_public_key")) + quoted_public_key = shlex.quote(public_key if public_key else "") + snapshot_workspace_command = ( + 'export RESTORE_DRILL_DIR="${RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill}"\n' + 'mkdir -p "$RESTORE_DRILL_DIR"\n' + 'chmod 700 "$RESTORE_DRILL_DIR"\n' + 'printf "Restore drill workspace: %s\\n" "$RESTORE_DRILL_DIR"' + ) + snapshot_command = ( + 'export RESTORE_DRILL_DIR="${RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill}"\n' + 'mkdir -p "$RESTORE_DRILL_DIR"\n' + 'chmod 700 "$RESTORE_DRILL_DIR"\n' + "printf 'OpenBao token: ' >&2\n" + "read -rs OPENBAO_TOKEN\n" + "printf '\\n' >&2\n" + 'printf \'%s\\n\' "$OPENBAO_TOKEN" | kubectl exec -i -n openbao openbao-0 -- ' + "sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao operator raft snapshot save /tmp/openbao-raft.snap'\n" + "unset OPENBAO_TOKEN\n" + 'kubectl cp openbao/openbao-0:/tmp/openbao-raft.snap "$RESTORE_DRILL_DIR/openbao-raft.snap"\n' + 'kubectl exec -n openbao openbao-0 -- rm -f /tmp/openbao-raft.snap\n' + 'sha256sum "$RESTORE_DRILL_DIR/openbao-raft.snap" | tee "$RESTORE_DRILL_DIR/openbao-raft.snap.sha256"' + ) + encrypt_snapshot_status = restore_status if public_key else "blocked" + encrypt_snapshot_reason = restore_reason if public_key else "Record the custodian public age recipient before encrypting snapshot custody material." + encrypt_snapshot_command = ( + 'export RESTORE_DRILL_DIR="${RESTORE_DRILL_DIR:-/tmp/netkingdom-openbao-restore-drill}"\n' + f'age -r {quoted_public_key} -o "$RESTORE_DRILL_DIR/openbao-raft.snap.age" "$RESTORE_DRILL_DIR/openbao-raft.snap"\n' + 'sha256sum "$RESTORE_DRILL_DIR/openbao-raft.snap.age" | tee "$RESTORE_DRILL_DIR/openbao-raft.snap.age.sha256"\n' + 'if command -v shred >/dev/null 2>&1; then\n' + ' shred -u "$RESTORE_DRILL_DIR/openbao-raft.snap"\n' + "else\n" + ' rm -f "$RESTORE_DRILL_DIR/openbao-raft.snap"\n' + "fi" + ) + isolated_restore_command = ( + "cat <<'RESTORE_DRILL'\n" + "Isolated Railiance OpenBao restore drill evidence required:\n" + "1. Use a disposable cluster, VM, or namespace. Do not restore into namespace=openbao.\n" + "2. Deploy a fresh OpenBao instance with empty storage.\n" + "3. Decrypt the encrypted snapshot only inside the isolated drill workspace.\n" + "4. Restore with: bao operator raft snapshot restore -force /tmp/openbao-raft.snap\n" + "5. Unseal the isolated instance with the current trial/drill shares.\n" + "6. Verify status, mounts, auth methods, policies, and a non-production test secret read.\n" + "7. Destroy the isolated environment and record only non-secret evidence in this UI.\n" + "RESTORE_DRILL" + ) + return [ add_taint( { @@ -1545,6 +1621,66 @@ def runbook_command_payloads(data: dict[str, Any]) -> list[dict[str, str]]: }, openbao_downstream_taint if initialized else {}, ), + add_taint( + { + "name": "Prepare restore drill workspace", + "description": "Create a local restricted directory for temporary snapshot evidence.", + "status": restore_status, + "status_reason": restore_reason, + "command": snapshot_workspace_command, + }, + restore_taint, + ), + add_taint( + { + "name": "Create encrypted-restore snapshot source", + "description": "Prompt locally for an OpenBao token, create a Raft snapshot in the pod, copy it out, remove the pod copy, and record a plaintext hash before encryption.", + "status": restore_status, + "status_reason": restore_reason, + "command": snapshot_command, + }, + restore_taint, + ), + add_taint( + { + "name": "Encrypt restore snapshot", + "description": "Encrypt the Raft snapshot to the custodian age recipient and remove the local plaintext snapshot.", + "status": encrypt_snapshot_status, + "status_reason": encrypt_snapshot_reason, + "command": encrypt_snapshot_command, + }, + restore_taint, + ), + add_taint( + { + "name": "Run isolated restore proof", + "description": "Checklist for proving the snapshot can restore into an isolated OpenBao instance before live secrets move in.", + "status": restore_status, + "status_reason": restore_reason, + "command": isolated_restore_command, + }, + restore_taint, + ), + add_taint( + { + "name": "Run post-restore readiness check", + "description": "Re-run the Railiance post-unseal checks after restore evidence has been captured.", + "status": restore_status, + "status_reason": restore_reason, + "command": "make -C ../railiance-platform openbao-verify-post-unseal", + }, + restore_taint, + ), + add_taint( + { + "name": "Record restore drill passed", + "description": "Non-secret metadata checkbox after encrypted snapshot evidence and isolated restore proof are complete.", + "status": restore_status, + "status_reason": restore_reason, + "command": "Use the checkbox: Restore drill passed", + }, + restore_taint, + ), ] 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 93a3833..3973047 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 @@ -263,6 +263,12 @@ Introduction & Actors, Subsystems & Scopes, Roles & Responsibilities, Integration & Tests, Artefacts & Locations, Usecases & Runbooks, and Terminology & Patterns. +**2026-05-25:** Added Restore drill runbook action cards so the existing +confirmation checkbox has a concrete path: prepare a restricted workspace, +create/copy/hash an OpenBao Raft snapshot, encrypt it to the custodian age +recipient, complete an isolated restore proof, rerun post-unseal verification, +and record only non-secret completion evidence. + **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