generated from coulomb/repo-seed
NET-WP-0020 finished: attended-ceremony + auto-unseal-transit profiles, greenfield init/unseal proof
T2: greenfield live proof against a fresh uninitialized OpenBao 2.5.5 — caught and fixed 'bao operator unseal -' not reading stdin (now 'bao write sys/unseal key=-'); init and reseal-replay paths proven. T3: attended-ceremony selectable — runbook, non-secret ceremony-record template + validator, and a lab/production deployment profile that blocks sops-held-automation in console selection, gates, and the init script. T4: console gate + evidence flags for auto-unseal-transit (Helm seal stanza prepared in railiance-platform). Also: SCOPE.md refreshed to current repo state; adhoc fix for the broken check-secrets Make target (unescaped $). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,30 +64,28 @@ OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS: dict[str, dict[str, str]] = {
|
||||
},
|
||||
"attended-ceremony": {
|
||||
"label": "Attended ceremony (production custody)",
|
||||
"implementation": "planned",
|
||||
"implementation": "implemented",
|
||||
"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."
|
||||
"railiance-platform/docs/openbao.md ceremony. Console never runs init; "
|
||||
"runbook + evidence validation only."
|
||||
),
|
||||
"runbook_entry": "docs/openbao-attended-ceremony-runbook.md",
|
||||
"custody_strength": "production",
|
||||
},
|
||||
"auto-unseal-transit": {
|
||||
"label": "Auto-unseal (transit/KMS seal)",
|
||||
"implementation": "planned",
|
||||
"implementation": "implemented",
|
||||
"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."
|
||||
"Seal config uses transit or cloud KMS; pod restart without manual unseal. "
|
||||
"Gate stays blocked until the seal stanza is applied and auto-unseal is verified."
|
||||
),
|
||||
"config_entry": "railiance-platform/helm/openbao-values.yaml (seal stanza)",
|
||||
"custody_strength": "production-ha",
|
||||
},
|
||||
}
|
||||
VALID_DEPLOYMENT_PROFILES = ("lab", "production")
|
||||
DEFAULT_DEPLOYMENT_PROFILE = "lab"
|
||||
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(
|
||||
@@ -461,14 +459,38 @@ def openbao_unseal_custody_model_implemented(model: str) -> bool:
|
||||
return model in IMPLEMENTED_OPENBAO_UNSEAL_CUSTODY_MODELS
|
||||
|
||||
|
||||
def resolve_deployment_profile(data: dict[str, Any]) -> str:
|
||||
profile = str(data.get("deployment_profile") or "").strip()
|
||||
if profile in VALID_DEPLOYMENT_PROFILES:
|
||||
return profile
|
||||
return DEFAULT_DEPLOYMENT_PROFILE
|
||||
|
||||
|
||||
def openbao_unseal_custody_model_entry(spec: dict[str, str]) -> str:
|
||||
for key in ("automation_entry", "runbook_entry", "config_entry"):
|
||||
if spec.get(key):
|
||||
return spec[key]
|
||||
return "n/a"
|
||||
|
||||
|
||||
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 resolve_deployment_profile(data) == "production" and model == "sops-held-automation":
|
||||
return Gate(
|
||||
"OpenBao unseal custody model",
|
||||
"blocked",
|
||||
(
|
||||
"Production profile blocks sops-held-automation (root token and unseal "
|
||||
"shares in one SOPS bundle is lab posture). Select attended-ceremony or "
|
||||
"auto-unseal-transit."
|
||||
),
|
||||
)
|
||||
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')}",
|
||||
f"{spec['label']} selected — entry: {openbao_unseal_custody_model_entry(spec)}",
|
||||
)
|
||||
return Gate(
|
||||
"OpenBao unseal custody model",
|
||||
@@ -489,6 +511,12 @@ def openbao_init_ceremony_gate(data: dict[str, Any]) -> Gate:
|
||||
spec.get("blocked_hint", "Selected unseal custody model is not implemented."),
|
||||
)
|
||||
if model == "sops-held-automation":
|
||||
if resolve_deployment_profile(data) == "production":
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
"Production profile blocks sops-held-automation. Select a production model.",
|
||||
)
|
||||
entry = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model].get("automation_entry", "")
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
@@ -498,10 +526,44 @@ def openbao_init_ceremony_gate(data: dict[str, Any]) -> Gate:
|
||||
f"({entry}). Console will not run init."
|
||||
),
|
||||
)
|
||||
if model == "auto-unseal-transit":
|
||||
if not yes(data, "openbao_transit_seal_configured"):
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
(
|
||||
"Transit/KMS seal not configured yet — enable the seal stanza in "
|
||||
"railiance-platform/helm/openbao-values.yaml and provision the "
|
||||
"transit/KMS backend, then set openbao_transit_seal_configured."
|
||||
),
|
||||
)
|
||||
if not yes(data, "openbao_auto_unseal_verified"):
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"blocked",
|
||||
(
|
||||
"Auto-unseal not verified — restart the OpenBao pod, confirm it "
|
||||
"unseals without shares, then set openbao_auto_unseal_verified."
|
||||
),
|
||||
)
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"human",
|
||||
(
|
||||
"Transit seal active. Attended `bao operator init` still required once "
|
||||
"(recovery keys, root retirement) — follow "
|
||||
"docs/openbao-attended-ceremony-runbook.md. Console will not run init."
|
||||
),
|
||||
)
|
||||
runbook = OPENBAO_UNSEAL_CUSTODY_MODEL_SPECS[model].get("runbook_entry", "")
|
||||
return Gate(
|
||||
"OpenBao init ceremony",
|
||||
"human",
|
||||
"Human-attended ceremony only. This console will not run init.",
|
||||
(
|
||||
"Human-attended ceremony only. This console will not run init. "
|
||||
f"Follow {runbook} and validate the non-secret record with "
|
||||
"validate-openbao-ceremony-record."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -740,12 +802,14 @@ def next_action(
|
||||
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 data and resolve_deployment_profile(data) == "production":
|
||||
return (
|
||||
"Select a production unseal custody model "
|
||||
"(attended-ceremony or auto-unseal-transit)"
|
||||
)
|
||||
return "Select an implemented openbao-unseal-custody-model"
|
||||
if gate.name == "OpenBao init ceremony":
|
||||
return "Select an implemented unseal custody model first"
|
||||
return "Resolve the OpenBao init ceremony gate (see gate reason)"
|
||||
if gate.name == "KeyCape OpenBao client definition":
|
||||
return "Ship KeyCape OpenBao client definition"
|
||||
if gate.name == "KeyCape OpenBao client deployed":
|
||||
@@ -821,6 +885,9 @@ def print_status(data: dict[str, Any]) -> None:
|
||||
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("20. select-deployment-profile")
|
||||
print("21. openbao-ceremony-record-template")
|
||||
print("22. validate-openbao-ceremony-record")
|
||||
print("")
|
||||
print("Refusal boundary")
|
||||
print("This console will not run bao operator init or collect secret values.")
|
||||
@@ -1934,6 +2001,10 @@ def metadata_template() -> dict[str, Any]:
|
||||
"progress_scope": "",
|
||||
"openbao_unseal_custody_model": DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL,
|
||||
"openbao_unseal_custody_model_selected_at": "",
|
||||
"deployment_profile": DEFAULT_DEPLOYMENT_PROFILE,
|
||||
"deployment_profile_selected_at": "",
|
||||
"openbao_transit_seal_configured": False,
|
||||
"openbao_auto_unseal_verified": False,
|
||||
"openbao_preflight_passed": False,
|
||||
"openbao_init_output_produced": False,
|
||||
"openbao_initialized": False,
|
||||
@@ -2002,7 +2073,7 @@ def print_openbao_unseal_custody_models() -> int:
|
||||
if status != "implemented":
|
||||
print(f" blocked_hint: {spec.get('blocked_hint', '')}")
|
||||
else:
|
||||
print(f" automation_entry: {spec.get('automation_entry', '')}")
|
||||
print(f" entry: {openbao_unseal_custody_model_entry(spec)}")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
@@ -2031,6 +2102,18 @@ def print_select_openbao_unseal_custody_model(args: argparse.Namespace, data: di
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
if model == "sops-held-automation" and resolve_deployment_profile(data) == "production":
|
||||
print("OPENBAO UNSEAL CUSTODY MODEL NOT SELECTABLE")
|
||||
print("")
|
||||
print(f"Model: {model} ({spec.get('label', '')})")
|
||||
print("Deployment profile: production")
|
||||
print("")
|
||||
print(
|
||||
"Production profile blocks sops-held-automation — root token and unseal "
|
||||
"shares in one SOPS bundle is lab posture. Select attended-ceremony or "
|
||||
"auto-unseal-transit, or switch back with: select-deployment-profile --profile lab"
|
||||
)
|
||||
return 1
|
||||
merged = metadata_template()
|
||||
merged.update(data)
|
||||
merged["openbao_unseal_custody_model"] = model
|
||||
@@ -2042,10 +2125,100 @@ def print_select_openbao_unseal_custody_model(args: argparse.Namespace, data: di
|
||||
print(f"Metadata: {args.metadata}")
|
||||
print(f"Model: {model}")
|
||||
print(f"Label: {spec.get('label', '')}")
|
||||
print(f"Automation entry: {spec.get('automation_entry', '')}")
|
||||
print(f"Entry: {openbao_unseal_custody_model_entry(spec)}")
|
||||
return 0
|
||||
|
||||
|
||||
def print_select_deployment_profile(args: argparse.Namespace, data: dict[str, Any]) -> int:
|
||||
profile = args.profile
|
||||
if args.metadata is None:
|
||||
print(
|
||||
"ERROR: select-deployment-profile requires --metadata /path/to/non-secret.json",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
merged = metadata_template()
|
||||
merged.update(data)
|
||||
merged["deployment_profile"] = profile
|
||||
merged["deployment_profile_selected_at"] = utc_now()
|
||||
merged["metadata_updated_at"] = utc_now()
|
||||
write_metadata(args.metadata, merged)
|
||||
print("DEPLOYMENT PROFILE SELECTED")
|
||||
print("")
|
||||
print(f"Metadata: {args.metadata}")
|
||||
print(f"Profile: {profile}")
|
||||
if profile == "production":
|
||||
model = resolve_openbao_unseal_custody_model(merged)
|
||||
print("")
|
||||
print("Production profile blocks sops-held-automation for OpenBao init/unseal.")
|
||||
if model == "sops-held-automation":
|
||||
print(
|
||||
f"WARNING: current unseal custody model is {model!r} — now blocked. "
|
||||
"Select attended-ceremony or auto-unseal-transit."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def openbao_ceremony_record_template() -> dict[str, Any]:
|
||||
return {
|
||||
"record_version": "v1",
|
||||
"evidence_date": "YYYY-MM-DD",
|
||||
"operator": "openbao-ceremony-operator",
|
||||
"runbook_reference": "docs/openbao-attended-ceremony-runbook.md",
|
||||
"ceremony_scope": "Attended OpenBao operator init on <cluster>, unseal share escrow, root token retirement.",
|
||||
"key_shares": 3,
|
||||
"key_threshold": 2,
|
||||
"unseal_share_escrow_disposition": "Describe where each share went (holder role + storage class) — never the shares themselves.",
|
||||
"root_token_disposition": "revoked-or-escrowed: describe.",
|
||||
"witness": "role or name of second attendee, or 'none' with justification",
|
||||
"attended_init_completed": False,
|
||||
"unseal_shares_escrowed_out_of_band": False,
|
||||
"root_token_retired_or_escrowed": False,
|
||||
"post_unseal_verified": False,
|
||||
"no_secret_material_recorded": False,
|
||||
}
|
||||
|
||||
|
||||
def print_openbao_ceremony_record_template() -> None:
|
||||
print(json.dumps(openbao_ceremony_record_template(), indent=2))
|
||||
|
||||
|
||||
def print_validate_openbao_ceremony_record(args: argparse.Namespace) -> int:
|
||||
evidence_path = resolve_cli_path(args.evidence)
|
||||
evidence, errors = load_evidence_json(evidence_path, "openbao-ceremony")
|
||||
if evidence is not None:
|
||||
errors.extend(
|
||||
require_evidence_fields(
|
||||
evidence,
|
||||
required_strings=(
|
||||
"evidence_date",
|
||||
"operator",
|
||||
"runbook_reference",
|
||||
"ceremony_scope",
|
||||
"unseal_share_escrow_disposition",
|
||||
"root_token_disposition",
|
||||
"witness",
|
||||
),
|
||||
required_true=(
|
||||
"attended_init_completed",
|
||||
"unseal_shares_escrowed_out_of_band",
|
||||
"root_token_retired_or_escrowed",
|
||||
"post_unseal_verified",
|
||||
"no_secret_material_recorded",
|
||||
),
|
||||
)
|
||||
)
|
||||
return print_validation_result(
|
||||
"OPENBAO ATTENDED CEREMONY RECORD VALIDATION",
|
||||
errors,
|
||||
[
|
||||
f"ceremony record valid: {evidence_path}",
|
||||
"record is non-secret (no secret-looking markers found)",
|
||||
"next: set openbao_initialized / openbao_post_unseal_verified in console metadata",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def gate_payload(gate: Gate) -> dict[str, str]:
|
||||
return {
|
||||
"name": gate.name,
|
||||
@@ -4974,6 +5147,29 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
default=DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL,
|
||||
help="Unseal custody model id.",
|
||||
)
|
||||
select_profile = sub.add_parser(
|
||||
"select-deployment-profile",
|
||||
help="Select deployment profile (production blocks sops-held-automation).",
|
||||
)
|
||||
select_profile.add_argument(
|
||||
"--profile",
|
||||
choices=list(VALID_DEPLOYMENT_PROFILES),
|
||||
required=True,
|
||||
help="Deployment profile id.",
|
||||
)
|
||||
sub.add_parser(
|
||||
"openbao-ceremony-record-template",
|
||||
help="Print non-secret attended OpenBao ceremony record JSON template.",
|
||||
)
|
||||
validate_ceremony = sub.add_parser(
|
||||
"validate-openbao-ceremony-record",
|
||||
help="Validate a non-secret attended OpenBao ceremony record.",
|
||||
)
|
||||
validate_ceremony.add_argument(
|
||||
"--evidence",
|
||||
default=".local/openbao-ceremony-record.json",
|
||||
help="Path to the non-secret ceremony record JSON.",
|
||||
)
|
||||
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.")
|
||||
@@ -5002,6 +5198,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"validate-cleanup",
|
||||
"approve-custody-mode",
|
||||
"select-openbao-unseal-custody-model",
|
||||
"select-deployment-profile",
|
||||
"web-ui",
|
||||
}
|
||||
if args.command in metadata_commands and args.metadata is None:
|
||||
@@ -5099,6 +5296,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return print_openbao_preflight(args)
|
||||
if args.command == "openbao-unseal-custody-models":
|
||||
return print_openbao_unseal_custody_models()
|
||||
if args.command == "select-deployment-profile":
|
||||
return print_select_deployment_profile(args, data)
|
||||
if args.command == "openbao-ceremony-record-template":
|
||||
print_openbao_ceremony_record_template()
|
||||
return 0
|
||||
if args.command == "validate-openbao-ceremony-record":
|
||||
return print_validate_openbao_ceremony_record(args)
|
||||
if args.command == "select-openbao-unseal-custody-model":
|
||||
return print_select_openbao_unseal_custody_model(args, data)
|
||||
if args.command == "web-ui":
|
||||
|
||||
Reference in New Issue
Block a user