feat: OpenBao unseal custody models — automation-first with blocked alternatives

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.
This commit is contained in:
2026-06-18 00:51:48 +02:00
parent da9debf431
commit f625dd0681
6 changed files with 460 additions and 12 deletions

View File

@@ -296,6 +296,15 @@ security-bootstrap-openbao-preflight: ## Show safe OpenBao preflight commands
python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight \ python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-preflight \
--railiance-path ../railiance-platform --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 security-bootstrap-metadata-init: ## Create durable local non-secret bootstrap metadata if missing
@mkdir -p "$$(dirname "$(SECURITY_BOOTSTRAP_METADATA)")" @mkdir -p "$$(dirname "$(SECURITY_BOOTSTRAP_METADATA)")"
@if [[ -f "$(SECURITY_BOOTSTRAP_METADATA)" ]]; then \ @if [[ -f "$(SECURITY_BOOTSTRAP_METADATA)" ]]; then \

View File

@@ -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-00150017).
- 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

View File

@@ -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). - Deploy client config (sso-mfa/k8s/keycape/create-secrets.sh).
- Apply keycape-config Secret, restart KeyCape. - Apply keycape-config Secret, restart KeyCape.
- Register bootstrap clients (netkingdom-bootstrap-console, openbao-admin). - 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. - Verify OIDC admin login: platform-root obtains OpenBao platform-admin via KeyCape/MFA.
- Evidence: keycape client gates, openbao_oidc_* , oidc_login_verified. - Evidence: keycape client gates, openbao_oidc_* , oidc_login_verified.
- Validate related in t02 / console. - 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 ## 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. - 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). - 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). - 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). - 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). **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. See NET-WP-0018 workplan for full acceptance.

View File

@@ -49,6 +49,52 @@ VALID_MFA_ENROLLMENT_SOURCES = {
} }
VALID_CUSTODY_MODES = {"temporary-single-king", "two-of-three-planned", "two-of-three-ready"} VALID_CUSTODY_MODES = {"temporary-single-king", "two-of-three-planned", "two-of-three-ready"}
CUSTODY_APPROVAL_MODES = {"temporary-single-king", "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" KEYCAPE_ISSUER = "https://kc.coulomb.social"
OIDC_CLIENT_ID = "netkingdom-bootstrap-console" OIDC_CLIENT_ID = "netkingdom-bootstrap-console"
OIDC_SCOPE = "openid profile email groups" 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") 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: def custody_mode_reason(data: dict[str, Any]) -> str:
mode = data.get("custody_mode") mode = data.get("custody_mode")
if mode in CUSTODY_APPROVAL_MODES and yes(data, "custody_mode_approved"): 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", "done" if yes(data, "openbao_preflight_passed") else "blocked",
"Run safe Railiance OpenBao status and verification checks.", "Run safe Railiance OpenBao status and verification checks.",
), ),
Gate( openbao_unseal_custody_model_gate(data),
"OpenBao init ceremony", openbao_init_ceremony_gate(data),
"human" if not yes(data, "openbao_initialized") else "done",
"Human-attended ceremony only. This console will not run init.",
),
Gate( Gate(
"OpenBao initial configuration", "OpenBao initial configuration",
( (
@@ -612,6 +710,10 @@ def next_action(
data: dict[str, Any] | None = None, data: dict[str, Any] | None = None,
) -> str: ) -> str:
for gate in gates: 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.status == "human":
if gate.name == "OpenBao init ceremony": if gate.name == "OpenBao init ceremony":
if data and yes(data, "openbao_init_output_produced") and not yes(data, "openbao_initialized"): 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" return "Approve custody strategy"
if gate.name == "OpenBao preflight": if gate.name == "OpenBao preflight":
return "Run 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": if gate.name == "KeyCape OpenBao client definition":
return "Ship KeyCape OpenBao client definition" return "Ship KeyCape OpenBao client definition"
if gate.name == "KeyCape OpenBao client deployed": 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("13. validate-custody-roster")
print("14. metadata-template") print("14. metadata-template")
print("15. approve-custody-mode") print("15. approve-custody-mode")
print("16. web-ui") print("16. openbao-unseal-custody-models")
print("17. validate-keycape-client (T08: example of validator-driven gate in UI state model)") 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("")
print("Refusal boundary") print("Refusal boundary")
print("This console will not run bao operator init or collect secret values.") 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, "oidc_login_verified": False,
"metadata_updated_at": "", "metadata_updated_at": "",
"progress_scope": "", "progress_scope": "",
"openbao_unseal_custody_model": DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL,
"openbao_unseal_custody_model_selected_at": "",
"openbao_preflight_passed": False, "openbao_preflight_passed": False,
"openbao_init_output_produced": False, "openbao_init_output_produced": False,
"openbao_initialized": False, "openbao_initialized": False,
@@ -1872,6 +1985,67 @@ def print_openbao_preflight(args: argparse.Namespace) -> int:
return 0 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]: def gate_payload(gate: Gate) -> dict[str, str]:
return { return {
"name": gate.name, "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") 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("handover-checklist", help="Print handover and cleanup checklist.")
sub.add_parser("metadata-template", help="Print non-secret metadata JSON template.") 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.") 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 = 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.") 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-t02",
"validate-cleanup", "validate-cleanup",
"approve-custody-mode", "approve-custody-mode",
"select-openbao-unseal-custody-model",
"web-ui", "web-ui",
} }
if args.command in metadata_commands and args.metadata is None: 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 return 0
if args.command == "openbao-preflight": if args.command == "openbao-preflight":
return print_openbao_preflight(args) 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": if args.command == "web-ui":
return serve_web_ui(args) return serve_web_ui(args)
if args.command == "refuse-live-init": if args.command == "refuse-live-init":

View File

@@ -16,9 +16,35 @@ SPEC.loader.exec_module(console)
def test_metadata_template_has_core_fields(): def test_metadata_template_has_core_fields():
tmpl = console.metadata_template() tmpl = console.metadata_template()
assert isinstance(tmpl, dict) 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: for f in core:
assert f in tmpl 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(): def test_onboarding_dry_run_template_has_required_fields():
tmpl = console.onboarding_dry_run_template() tmpl = console.onboarding_dry_run_template()

View File

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