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:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user