From 123b9aafce00b83b45c6a88e87db385cac0a8d51 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 1 Jun 2026 23:57:00 +0200 Subject: [PATCH] Add OpenBao restore evidence validator --- Makefile | 7 +- ...penbao-restore-drill-evidence.example.json | 22 ++++ docs/openbao.md | 9 ++ scripts/openbao-validate-restore-evidence.sh | 112 ++++++++++++++++++ ...P-0002-openbao-platform-secrets-service.md | 7 ++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/openbao-restore-drill-evidence.example.json create mode 100755 scripts/openbao-validate-restore-evidence.sh diff --git a/Makefile b/Makefile index 8aacd83..5d4da3f 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ OPENBAO_NAMESPACE ?= openbao OPENBAO_RELEASE ?= openbao OPENBAO_VALUES ?= helm/openbao-values.yaml OPENBAO_VERIFY_AUTH_ARGS ?= +OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json ##@ CloudNative PG (cnpg) — primary database operator @@ -126,6 +127,10 @@ openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/au KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS) +openbao-validate-restore-evidence: ## Validate non-secret OpenBao restore-drill evidence JSON + OPENBAO_RESTORE_EVIDENCE='$(OPENBAO_RESTORE_EVIDENCE)' \ + scripts/openbao-validate-restore-evidence.sh + ##@ Backup backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud @@ -138,4 +143,4 @@ help: ## Show this help /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) -.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-verify-authenticated backup help +.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-verify-authenticated openbao-validate-restore-evidence backup help diff --git a/docs/openbao-restore-drill-evidence.example.json b/docs/openbao-restore-drill-evidence.example.json new file mode 100644 index 0000000..1950195 --- /dev/null +++ b/docs/openbao-restore-drill-evidence.example.json @@ -0,0 +1,22 @@ +{ + "drill_date": "2026-06-01", + "operator": "platform-root", + "source_cluster": "railiance01", + "source_namespace": "openbao", + "source_pod": "openbao-0", + "snapshot_created": true, + "snapshot_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "snapshot_encrypted": true, + "encrypted_snapshot_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "encrypted_snapshot_location": "operator-local encrypted restore drill workspace or approved encrypted custody location", + "isolated_environment": "disposable cluster, VM, or namespace reference", + "isolated_restore_completed": true, + "unseal_verified_in_isolation": true, + "test_secret_read_verified": true, + "post_restore_status_verified": true, + "post_restore_verification": "bao status, mount/auth/policy checks, and a non-production test secret read succeeded in the isolated environment", + "isolated_environment_destroyed": true, + "destroyed_environment_evidence": "operator note, VM deletion id, namespace deletion timestamp, or equivalent non-secret proof", + "no_secret_material_recorded": true, + "notes": "Do not record OpenBao tokens, root tokens, unseal shares, decrypted snapshots, private keys, passwords, OTP seeds, or recovery codes." +} diff --git a/docs/openbao.md b/docs/openbao.md index f9dba30..0e72712 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -271,6 +271,15 @@ Before any live application secrets move into OpenBao: 4. Run an isolated restore drill before treating OpenBao as live secret custody. The drill must prove that a fresh OpenBao instance can restore the snapshot, unseal, and read a test secret. + Record only non-secret evidence using + `docs/openbao-restore-drill-evidence.example.json` as a template, then + validate it with: + + ```bash + make openbao-validate-restore-evidence \ + OPENBAO_RESTORE_EVIDENCE=/path/to/evidence.json + ``` + 5. Decide where audit logs are shipped durably. The audit PVC alone is not a durable audit sink. The interim `audit-core` mock file backend can prove API and setup wiring, but it writes to `/tmp` and is not production retention. diff --git a/scripts/openbao-validate-restore-evidence.sh b/scripts/openbao-validate-restore-evidence.sh new file mode 100755 index 0000000..eb102fc --- /dev/null +++ b/scripts/openbao-validate-restore-evidence.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +EVIDENCE="${OPENBAO_RESTORE_EVIDENCE:-${1:-/tmp/netkingdom-openbao-restore-drill/evidence.json}}" + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-validate-restore-evidence.sh [evidence.json] + +Validates non-secret OpenBao restore-drill evidence. The evidence file should +record hashes, dates, isolated environment references, and verification flags, +but must not contain OpenBao tokens, unseal shares, decrypted snapshots, private +keys, passwords, OTP seeds, or recovery codes. + +Environment: + OPENBAO_RESTORE_EVIDENCE Evidence JSON path. Default: + /tmp/netkingdom-openbao-restore-drill/evidence.json +USAGE +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +python3 - "$EVIDENCE" <<'PY' +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +if not path.exists(): + print(f"[FAIL] restore evidence file is missing: {path}", file=sys.stderr) + sys.exit(1) + +try: + data = json.loads(path.read_text(encoding="utf-8")) +except json.JSONDecodeError as exc: + print(f"[FAIL] restore evidence is not valid JSON: {exc}", file=sys.stderr) + sys.exit(1) + +if not isinstance(data, dict): + print("[FAIL] restore evidence root must be a JSON object", file=sys.stderr) + sys.exit(1) + +required_strings = [ + "drill_date", + "operator", + "source_cluster", + "source_namespace", + "source_pod", + "isolated_environment", + "snapshot_sha256", + "encrypted_snapshot_sha256", + "encrypted_snapshot_location", + "post_restore_verification", + "destroyed_environment_evidence", +] +required_true = [ + "snapshot_created", + "snapshot_encrypted", + "isolated_restore_completed", + "unseal_verified_in_isolation", + "test_secret_read_verified", + "post_restore_status_verified", + "isolated_environment_destroyed", + "no_secret_material_recorded", +] + +errors: list[str] = [] +for key in required_strings: + value = data.get(key) + if not isinstance(value, str) or not value.strip(): + errors.append(f"missing non-empty string: {key}") + +sha_pattern = re.compile(r"^(sha256:)?[0-9a-fA-F]{64}$") +for key in ("snapshot_sha256", "encrypted_snapshot_sha256"): + value = str(data.get(key, "")) + if value and not sha_pattern.match(value): + errors.append(f"{key} must be a sha256 hex digest, optionally prefixed with sha256:") + +for key in required_true: + if data.get(key) is not True: + errors.append(f"must be true: {key}") + +encoded = json.dumps(data, sort_keys=True) +secret_markers = [ + "OPENBAO_ROOT_TOKEN", + "VAULT_TOKEN", + "BEGIN PRIVATE KEY", + "BEGIN OPENSSH PRIVATE KEY", + "AGE-SECRET-KEY-1", + "-----BEGIN", + "hvs.", +] +for marker in secret_markers: + if marker in encoded: + errors.append(f"secret-looking marker present: {marker}") + +if errors: + for error in errors: + print(f"[FAIL] {error}", file=sys.stderr) + sys.exit(1) + +print(f"[OK] restore evidence is structurally valid: {path}") +print(f"[OK] drill_date: {data['drill_date']}") +print(f"[OK] source: {data['source_cluster']}/{data['source_namespace']}/{data['source_pod']}") +print(f"[OK] isolated_environment: {data['isolated_environment']}") +PY diff --git a/workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md b/workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md index c32669e..03a676b 100644 --- a/workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md +++ b/workplans/RAIL-PL-WP-0002-openbao-platform-secrets-service.md @@ -294,6 +294,13 @@ days; it is suitable for interface wiring and setup validation only. Railiance still owns the OpenBao file audit device and PVC, while production retention, tenant policy, and tamper-evident archive belong to Audit Core. +**2026-06-01:** Added a non-secret OpenBao restore-drill evidence template and +`make openbao-validate-restore-evidence`. The validator requires concrete +review evidence such as snapshot hashes, encrypted snapshot location, isolated +restore completion, unseal/status/test-secret verification, isolated +environment destruction, and a `no_secret_material_recorded` assertion. This +keeps `NET-WP-0017-T02` from relying on a bare UI checkbox for restore proof. + ### T07 - Cross-Repo Transition Tasks ```task