generated from coulomb/repo-seed
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:
9
Makefile
9
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 \
|
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 \
|
||||||
|
|||||||
105
docs/openbao-unseal-custody-models.md
Normal file
105
docs/openbao-unseal-custody-models.md
Normal 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-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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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`
|
||||||
Reference in New Issue
Block a user