From f625dd0681d4e70add4b6fd77778b70a845baf56 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 18 Jun 2026 00:51:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20OpenBao=20unseal=20custody=20models=20?= =?UTF-8?q?=E2=80=94=20automation-first=20with=20blocked=20alternatives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document three init/unseal custody paths; default sops-held-automation for fast rebuild cycles. Security bootstrap console lists models, blocks planned attended-ceremony and auto-unseal-transit with hints, and gates init ceremony on implemented selection. NET-WP-0020 tracks downstream SSH automation. --- Makefile | 9 + docs/openbao-unseal-custody-models.md | 105 +++++++++ docs/smooth-bootstrap-guide.md | 29 ++- .../security_bootstrap_console.py | 207 +++++++++++++++++- .../tests/test_security_bootstrap_console.py | 28 ++- ...enbao-unseal-custody-and-ssh-automation.md | 94 ++++++++ 6 files changed, 460 insertions(+), 12 deletions(-) create mode 100644 docs/openbao-unseal-custody-models.md create mode 100644 workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md diff --git a/Makefile b/Makefile index 2e6009e..9c8454f 100644 --- a/Makefile +++ b/Makefile @@ -296,6 +296,15 @@ security-bootstrap-openbao-preflight: ## Show safe OpenBao preflight commands python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight \ --railiance-path ../railiance-platform +security-bootstrap-openbao-unseal-custody-models: ## List OpenBao unseal custody models and implementation status + python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-unseal-custody-models + +security-bootstrap-select-openbao-unseal-custody-model: security-bootstrap-metadata-init ## Select implemented unseal model (blocks planned): make ... MODEL=sops-held-automation + python3 tools/security-bootstrap-console/security_bootstrap_console.py \ + --metadata "$(SECURITY_BOOTSTRAP_METADATA)" \ + select-openbao-unseal-custody-model \ + --model "$(if $(MODEL),$(MODEL),sops-held-automation)" + security-bootstrap-metadata-init: ## Create durable local non-secret bootstrap metadata if missing @mkdir -p "$$(dirname "$(SECURITY_BOOTSTRAP_METADATA)")" @if [[ -f "$(SECURITY_BOOTSTRAP_METADATA)" ]]; then \ diff --git a/docs/openbao-unseal-custody-models.md b/docs/openbao-unseal-custody-models.md new file mode 100644 index 0000000..867d6c9 --- /dev/null +++ b/docs/openbao-unseal-custody-models.md @@ -0,0 +1,105 @@ +# OpenBao Unseal Custody Models + +Date: 2026-06-17 +Status: framework — automation path active; production paths planned + +NetKingdom bootstrap must support **three** OpenBao init/unseal custody models. +Development starts with **maximum automation** for fast test cycles, then adds +human custody gates as production trust increases. + +This is separate from **king custody mode** (`temporary-single-king`, +`two-of-three-planned`, `two-of-three-ready`) which governs who holds platform +recovery authority. Unseal custody models govern **how init/unseal executes** +during bootstrap and rebuild. + +--- + +## Models + +| Model ID | Label | Custody strength | Automation | Status | +| --- | --- | --- | --- | --- | +| `sops-held-automation` | SOPS-held unseal | Lab / fast iteration | High | **Implemented** (console + creds agent path) | +| `attended-ceremony` | Attended ceremony | Production | Low | Planned | +| `auto-unseal-transit` | Auto-unseal (transit/KMS) | Production HA | High | Planned | + +### `sops-held-automation` (default for greenfield dev) + +- Init/unseal material lives in **SOPS/age** custody bundle (not Git plaintext). +- Applied by `sso-mfa/bootstrap/creds-bootstrap-agent.sh` and related `creds-apply` + tooling after cluster + OpenBao pod exist. +- Enables **unattended rebuild test cycles** on a 3-node slate. +- **Not** production trust posture — use to prove S1→S3→SSH engine automation, + then graduate to stronger models. + +### `attended-ceremony` (production target) + +- Human-attended `bao operator init`, out-of-band unseal share escrow, root token + retirement — per `railiance-platform/docs/openbao.md`. +- Matches first successful NetKingdom bootstrap (NET-WP-0015–0017). +- Console keeps **refuse-live-init** boundary; ceremony runbooks only. + +### `auto-unseal-transit` (production HA target) + +- OpenBao seal configuration uses **transit** or cloud KMS auto-unseal. +- Pod restart without manual unseal threshold ceremony. +- Requires `railiance-platform` Helm seal stanza + KMS provisioning. + +--- + +## Development strategy + +```text +1. Implement automation path (sops-held-automation) + → SSH engine, warden sign, host CA trust, 3-node rebuild loops +2. Add attended-ceremony gates (block automation defaults in production profile) +3. Add auto-unseal-transit for HA ThreePhoenix rebuilds +``` + +Each model is selectable in the **security bootstrap console**. Unimplemented +models are **blocked** with a hint pointing to the active automation path. + +--- + +## Console integration + +```bash +# List models and implementation status +python3 tools/security-bootstrap-console/security_bootstrap_console.py \ + openbao-unseal-custody-models + +# Select active model (only implemented models succeed) +python3 tools/security-bootstrap-console/security_bootstrap_console.py \ + select-openbao-unseal-custody-model \ + --model sops-held-automation \ + --metadata .local/security-bootstrap.json + +# Status shows gate: "OpenBao unseal custody model" +make security-bootstrap-console # or: ... status --metadata .local/... +``` + +Metadata field: `openbao_unseal_custody_model` + +--- + +## Automation chain (after model selected) + +| Step | Owner | Target | +| --- | --- | --- | +| S1 OS baseline | railiance-infra | 3 nodes | +| S2 k3s HA | railiance-cluster | ThreePhoenix | +| S3 OpenBao deploy | railiance-platform | `make openbao-deploy` | +| Init/unseal apply | net-kingdom | `creds-bootstrap-agent.sh` (sops-held) | +| Platform config | railiance-platform | `openbao-configure-initial` | +| SSH engine | railiance-platform | `openbao-configure-ssh` (planned) | +| Host CA trust | railiance-infra | `bootstrap-ssh-ca` (planned) | +| Sign smoke | ops-warden | `warden sign` (WP-0008 T2) | + +--- + +## Related docs + +- `docs/smooth-bootstrap-guide.md` — Step 5 (OpenBao init/unseal) +- `docs/platform-root-custody.md` — king / quorum custody +- `railiance-platform/docs/openbao.md` — deploy and ceremony +- `ops-warden/wiki/OpenBaoSshEngineChecklist.md` — SSH engine verify +- `ops-warden/history/2026-06-17-openbao-production-verify.md` — current blockers \ No newline at end of file diff --git a/docs/smooth-bootstrap-guide.md b/docs/smooth-bootstrap-guide.md index d38b075..bd1aae7 100644 --- a/docs/smooth-bootstrap-guide.md +++ b/docs/smooth-bootstrap-guide.md @@ -94,7 +94,9 @@ See T03 retrospective for past realm drift bumps (now partially automated via ru - Deploy client config (sso-mfa/k8s/keycape/create-secrets.sh). - Apply keycape-config Secret, restart KeyCape. - Register bootstrap clients (netkingdom-bootstrap-console, openbao-admin). -- Redirects: localhost:8250/oidc/callback etc. +- OpenBao admin redirects: localhost CLI callbacks plus + `https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback` for + preferred browser UI login; `keycape` remains a compatibility auth mount. - Verify OIDC admin login: platform-root obtains OpenBao platform-admin via KeyCape/MFA. - Evidence: keycape client gates, openbao_oidc_* , oidc_login_verified. - Validate related in t02 / console. @@ -105,9 +107,28 @@ See T03 for past callback/registration bumps (now gated). ## Step 5: OpenBao Init / Unseal / Config + OIDC Admin Binding -**Attended only (console refuses live init):** +**Unseal custody model (select first):** see `docs/openbao-unseal-custody-models.md`. + +| Model | When | Console | +| --- | --- | --- | +| `sops-held-automation` | **Default** — fast rebuild test cycles | Selectable; gate `automation` | +| `attended-ceremony` | Production trust | Blocked until implemented | +| `auto-unseal-transit` | Production HA | Blocked until implemented | + +```bash +make security-bootstrap-openbao-unseal-custody-models +make security-bootstrap-select-openbao-unseal-custody-model MODEL=sops-held-automation +``` + +Development strategy: prove automation path first (SSH engine, warden, host CA), +then add attended and auto-unseal gates for production profiles. + +**Console refuses live init in all models** — automation runs via `creds-bootstrap-agent.sh` +(sops-held) or future attended/auto-unseal playbooks. + - Preflight: `make security-bootstrap-openbao-preflight --run` or console. -- Init ceremony (human-attended): produce init output, unseal shares, root token. +- Init/unseal: **sops-held-automation** → `sso-mfa/bootstrap/creds-bootstrap-agent.sh`; + **attended-ceremony** → human ceremony (planned automation hooks). - Post-unseal: apply initial config (auth, mounts, policies, audit). - OIDC auth config against KeyCape (maps claims/groups to policies e.g. net-kingdom-admins → platform-admin). - Key material handling: trial exposure taint, rotate unseal keys, emergency lockdown, restore drill (snapshot, isolate, verify, destroy). @@ -201,4 +222,4 @@ This guide + the runtime architecture + retrospective turn the first bootstrap i **Next after this guide:** Align control surface (T06), add tests (T07), integrate validations (T08), assess rebuild risk (T09). -See NET-WP-0018 workplan for full acceptance. \ No newline at end of file +See NET-WP-0018 workplan for full acceptance. diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index e089712..318f23d 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -49,6 +49,52 @@ VALID_MFA_ENROLLMENT_SOURCES = { } VALID_CUSTODY_MODES = {"temporary-single-king", "two-of-three-planned", "two-of-three-ready"} CUSTODY_APPROVAL_MODES = {"temporary-single-king", "two-of-three-ready"} + +# OpenBao init/unseal custody — how bootstrap applies init/unseal (separate from king custody_mode). +OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS: dict[str, dict[str, str]] = { + "sops-held-automation": { + "label": "SOPS-held unseal (automation-optimized)", + "implementation": "implemented", + "summary": ( + "Init/unseal material in SOPS/age bundle; creds-bootstrap-agent applies " + "for fast unattended rebuild test cycles." + ), + "automation_entry": "sso-mfa/bootstrap/creds-bootstrap-agent.sh", + "custody_strength": "lab-fast-iteration", + }, + "attended-ceremony": { + "label": "Attended ceremony (production custody)", + "implementation": "planned", + "summary": ( + "Human-attended init, out-of-band unseal escrow, root retirement — " + "railiance-platform/docs/openbao.md ceremony." + ), + "blocked_hint": ( + "Not yet implemented in the automation path. Use sops-held-automation for " + "fast bootstrap test cycles; attended ceremony will gate production trust." + ), + "custody_strength": "production", + }, + "auto-unseal-transit": { + "label": "Auto-unseal (transit/KMS seal)", + "implementation": "planned", + "summary": ( + "Seal config uses transit or cloud KMS; pod restart without manual unseal." + ), + "blocked_hint": ( + "Not yet implemented. Requires railiance-platform Helm seal stanza and " + "KMS/transit provisioning. Use sops-held-automation until available." + ), + "custody_strength": "production-ha", + }, +} +VALID_OPENBAO_UNSEAL_CUSTODY_MODELS = frozenset(OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS) +DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL = "sops-held-automation" +IMPLEMENTED_OPENBAO_UNSEAL_CUSTODY_MODELS = frozenset( + model + for model, spec in OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS.items() + if spec.get("implementation") == "implemented" +) KEYCAPE_ISSUER = "https://kc.coulomb.social" OIDC_CLIENT_ID = "netkingdom-bootstrap-console" OIDC_SCOPE = "openid profile email groups" @@ -404,6 +450,61 @@ def custody_mode_approved(data: dict[str, Any]) -> bool: return data.get("custody_mode") in CUSTODY_APPROVAL_MODES and yes(data, "custody_mode_approved") +def resolve_openbao_unseal_custody_model(data: dict[str, Any]) -> str: + model = str(data.get("openbao_unseal_custody_model") or "").strip() + if model in VALID_OPENBAO_UNSEAL_CUSTODY_MODELS: + return model + return DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL + + +def openbao_unseal_custody_model_implemented(model: str) -> bool: + return model in IMPLEMENTED_OPENBAO_UNSEAL_CUSTODY_MODELS + + +def openbao_unseal_custody_model_gate(data: dict[str, Any]) -> Gate: + model = resolve_openbao_unseal_custody_model(data) + spec = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model] + if openbao_unseal_custody_model_implemented(model): + return Gate( + "OpenBao unseal custody model", + "done", + f"{spec['label']} selected — automation entry: {spec.get('automation_entry', 'n/a')}", + ) + return Gate( + "OpenBao unseal custody model", + "blocked", + spec.get("blocked_hint", "Selected model is not implemented."), + ) + + +def openbao_init_ceremony_gate(data: dict[str, Any]) -> Gate: + if yes(data, "openbao_initialized"): + return Gate("OpenBao init ceremony", "done", "OpenBao initialized and unsealed.") + model = resolve_openbao_unseal_custody_model(data) + if not openbao_unseal_custody_model_implemented(model): + spec = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model] + return Gate( + "OpenBao init ceremony", + "blocked", + spec.get("blocked_hint", "Selected unseal custody model is not implemented."), + ) + if model == "sops-held-automation": + entry = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model].get("automation_entry", "") + return Gate( + "OpenBao init ceremony", + "automation", + ( + "Run unattended init/unseal via SOPS-held bundle " + f"({entry}). Console will not run init." + ), + ) + return Gate( + "OpenBao init ceremony", + "human", + "Human-attended ceremony only. This console will not run init.", + ) + + def custody_mode_reason(data: dict[str, Any]) -> str: mode = data.get("custody_mode") if mode in CUSTODY_APPROVAL_MODES and yes(data, "custody_mode_approved"): @@ -509,11 +610,8 @@ def build_gates(data: dict[str, Any]) -> list[Gate]: "done" if yes(data, "openbao_preflight_passed") else "blocked", "Run safe Railiance OpenBao status and verification checks.", ), - Gate( - "OpenBao init ceremony", - "human" if not yes(data, "openbao_initialized") else "done", - "Human-attended ceremony only. This console will not run init.", - ), + openbao_unseal_custody_model_gate(data), + openbao_init_ceremony_gate(data), Gate( "OpenBao initial configuration", ( @@ -612,6 +710,10 @@ def next_action( data: dict[str, Any] | None = None, ) -> str: for gate in gates: + if gate.status == "automation": + if gate.name == "OpenBao init ceremony": + return "Run SOPS-held OpenBao init/unseal automation" + return gate.name if gate.status == "human": if gate.name == "OpenBao init ceremony": if data and yes(data, "openbao_init_output_produced") and not yes(data, "openbao_initialized"): @@ -637,6 +739,13 @@ def next_action( return "Approve custody strategy" if gate.name == "OpenBao preflight": return "Run OpenBao preflight" + if gate.name == "OpenBao unseal custody model": + return ( + "Select openbao-unseal-custody-model sops-held-automation " + "(other models not yet implemented)" + ) + if gate.name == "OpenBao init ceremony": + return "Select an implemented unseal custody model first" if gate.name == "KeyCape OpenBao client definition": return "Ship KeyCape OpenBao client definition" if gate.name == "KeyCape OpenBao client deployed": @@ -708,8 +817,10 @@ def print_status(data: dict[str, Any]) -> None: print("13. validate-custody-roster") print("14. metadata-template") print("15. approve-custody-mode") - print("16. web-ui") - print("17. validate-keycape-client (T08: example of validator-driven gate in UI state model)") + print("16. openbao-unseal-custody-models") + print("17. select-openbao-unseal-custody-model") + print("18. web-ui") + print("19. validate-keycape-client (T08: example of validator-driven gate in UI state model)") print("") print("Refusal boundary") print("This console will not run bao operator init or collect secret values.") @@ -1821,6 +1932,8 @@ def metadata_template() -> dict[str, Any]: "oidc_login_verified": False, "metadata_updated_at": "", "progress_scope": "", + "openbao_unseal_custody_model": DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL, + "openbao_unseal_custody_model_selected_at": "", "openbao_preflight_passed": False, "openbao_init_output_produced": False, "openbao_initialized": False, @@ -1872,6 +1985,67 @@ def print_openbao_preflight(args: argparse.Namespace) -> int: return 0 +def print_openbao_unseal_custody_models() -> int: + print("OPENBAO UNSEAL CUSTODY MODELS") + print("") + print(f"Default (automation-first): {DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL}") + print("See docs/openbao-unseal-custody-models.md") + print("") + for model in sorted(OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS): + spec = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model] + status = spec.get("implementation", "planned") + print(f"- {model}") + print(f" label: {spec.get('label', '')}") + print(f" implementation: {status}") + print(f" custody_strength: {spec.get('custody_strength', '')}") + print(f" summary: {spec.get('summary', '')}") + if status != "implemented": + print(f" blocked_hint: {spec.get('blocked_hint', '')}") + else: + print(f" automation_entry: {spec.get('automation_entry', '')}") + print("") + return 0 + + +def print_select_openbao_unseal_custody_model(args: argparse.Namespace, data: dict[str, Any]) -> int: + model = args.model + if model not in VALID_OPENBAO_UNSEAL_CUSTODY_MODELS: + print(f"ERROR: unknown model {model!r}", file=sys.stderr) + return 2 + spec = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model] + if not openbao_unseal_custody_model_implemented(model): + print("OPENBAO UNSEAL CUSTODY MODEL NOT SELECTABLE") + print("") + print(f"Model: {model} ({spec.get('label', '')})") + print(f"Status: {spec.get('implementation', 'planned')} — not yet implemented") + print("") + print(spec.get("blocked_hint", "This model is not available yet.")) + print("") + print( + f"Use: select-openbao-unseal-custody-model --model {DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL}" + ) + return 1 + if args.metadata is None: + print( + "ERROR: select-openbao-unseal-custody-model requires --metadata /path/to/non-secret.json", + file=sys.stderr, + ) + return 2 + merged = metadata_template() + merged.update(data) + merged["openbao_unseal_custody_model"] = model + merged["openbao_unseal_custody_model_selected_at"] = utc_now() + merged["metadata_updated_at"] = utc_now() + write_metadata(args.metadata, merged) + print("OPENBAO UNSEAL CUSTODY MODEL SELECTED") + print("") + print(f"Metadata: {args.metadata}") + print(f"Model: {model}") + print(f"Label: {spec.get('label', '')}") + print(f"Automation entry: {spec.get('automation_entry', '')}") + return 0 + + def gate_payload(gate: Gate) -> dict[str, str]: return { "name": gate.name, @@ -4786,6 +4960,20 @@ def build_parser() -> argparse.ArgumentParser: claims.add_argument("--groups", default="net-kingdom-users", help="Comma-separated groups during dry-run") sub.add_parser("handover-checklist", help="Print handover and cleanup checklist.") sub.add_parser("metadata-template", help="Print non-secret metadata JSON template.") + sub.add_parser( + "openbao-unseal-custody-models", + help="List OpenBao init/unseal custody models and implementation status.", + ) + select_unseal = sub.add_parser( + "select-openbao-unseal-custody-model", + help="Select an implemented OpenBao unseal custody model (blocks planned models).", + ) + select_unseal.add_argument( + "--model", + choices=sorted(VALID_OPENBAO_UNSEAL_CUSTODY_MODELS), + default=DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL, + help="Unseal custody model id.", + ) sub.add_parser("refuse-live-init", help="Explain why live OpenBao init is refused.") web = sub.add_parser("web-ui", help="Serve a local custody approval UI.") web.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to localhost.") @@ -4813,6 +5001,7 @@ def main(argv: list[str] | None = None) -> int: "validate-t02", "validate-cleanup", "approve-custody-mode", + "select-openbao-unseal-custody-model", "web-ui", } if args.command in metadata_commands and args.metadata is None: @@ -4908,6 +5097,10 @@ def main(argv: list[str] | None = None) -> int: return 0 if args.command == "openbao-preflight": return print_openbao_preflight(args) + if args.command == "openbao-unseal-custody-models": + return print_openbao_unseal_custody_models() + if args.command == "select-openbao-unseal-custody-model": + return print_select_openbao_unseal_custody_model(args, data) if args.command == "web-ui": return serve_web_ui(args) if args.command == "refuse-live-init": diff --git a/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py b/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py index a41aac7..d1eaa4c 100644 --- a/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py +++ b/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py @@ -16,9 +16,35 @@ SPEC.loader.exec_module(console) def test_metadata_template_has_core_fields(): tmpl = console.metadata_template() assert isinstance(tmpl, dict) - core = ["approval_scope", "bootstrap_mode", "custody_mode", "review_date"] + core = [ + "approval_scope", + "bootstrap_mode", + "custody_mode", + "openbao_unseal_custody_model", + "review_date", + ] for f in core: assert f in tmpl + assert tmpl["openbao_unseal_custody_model"] == console.DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL + + +def test_openbao_unseal_custody_model_gate_automation_default(): + data = console.metadata_template() + gate = console.openbao_unseal_custody_model_gate(data) + assert gate.status == "done" + init_gate = console.openbao_init_ceremony_gate(data) + assert init_gate.status == "automation" + + +def test_openbao_unseal_custody_planned_models_blocked(): + for model in ("attended-ceremony", "auto-unseal-transit"): + data = console.metadata_template() + data["openbao_unseal_custody_model"] = model + gate = console.openbao_unseal_custody_model_gate(data) + assert gate.status == "blocked" + assert "not yet implemented" in gate.reason.lower() + init_gate = console.openbao_init_ceremony_gate(data) + assert init_gate.status == "blocked" def test_onboarding_dry_run_template_has_required_fields(): tmpl = console.onboarding_dry_run_template() diff --git a/workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md b/workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md new file mode 100644 index 0000000..9f1db86 --- /dev/null +++ b/workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md @@ -0,0 +1,94 @@ +--- +id: NET-WP-0020 +type: workplan +title: "OpenBao Unseal Custody Models and SSH Automation Path" +domain: net-kingdom +repo: net-kingdom +status: active +owner: codex +topic_slug: net-kingdom +created: "2026-06-17" +updated: "2026-06-17" +--- + +# NET-WP-0020 — OpenBao Unseal Custody Models and SSH Automation Path + +**Scope:** Framework for three OpenBao init/unseal custody models; automation-first +development path; console decision points; downstream hooks for SSH engine and +host CA automation on greenfield 3-node bootstrap. + +**Strategy:** Start with `sops-held-automation` for fast unattended test cycles; +add `attended-ceremony` and `auto-unseal-transit` with blocking gates as +production trust increases. + +--- + +## Tasks + +### T1 — Custody model canon and console gates + +```task +id: NET-WP-0020-T01 +status: done +priority: high +``` + +- [x] `docs/openbao-unseal-custody-models.md` +- [x] Console: list + select commands; gates block planned models +- [x] `smooth-bootstrap-guide.md` Step 5 update +- [x] Makefile targets + +### T2 — SOPS-held init/unseal automation hooks + +```task +id: NET-WP-0020-T02 +status: todo +priority: high +``` + +- [ ] Extend `creds-bootstrap-agent.sh` for OpenBao init/unseal when sealed +- [ ] Non-secret evidence flags: `openbao_initialized`, `openbao_post_unseal_verified` +- [ ] Integrate with `make openbao-configure-initial` post-unseal + +### T3 — Attended ceremony automation profile + +```task +id: NET-WP-0020-T03 +status: wait +priority: medium +``` + +- [ ] Implement `attended-ceremony` selection path (runbooks + evidence validators) +- [ ] Production profile blocks `sops-held-automation` default + +**Blocked until:** T2 automation path proven on greenfield rebuild. + +### T4 — Auto-unseal transit profile + +```task +id: NET-WP-0020-T04 +status: wait +priority: medium +``` + +- [ ] `railiance-platform` Helm seal stanza for transit/KMS +- [ ] Console gate + evidence for `auto-unseal-transit` + +### T5 — SSH engine + host CA automation (cross-repo) + +```task +id: NET-WP-0020-T05 +status: todo +priority: high +``` + +- [ ] `railiance-platform`: `openbao-configure-ssh` declarative script +- [ ] `railiance-infra`: `bootstrap-ssh-ca` role + inventory sync +- [ ] Close `ops-warden` WP-0008 T2 verification gate + +--- + +## See also + +- `ops-warden/workplans/WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md` +- `railiance-platform/docs/openbao.md` \ No newline at end of file