MASON-WP-0015-T04: structure-only AppRole build, post-build policy check
build_approle_kv_lane no longer reads role-id or mints secret-id; deliver_approle_credentials is the separate founder/applier step. New policies are read back and compared before the role is written. The Kubernetes build uses token_policies/token_ttl (legacy policies is denied by ops-mason-construct). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 278674@bnt-lap001 Assistant-Session: 64827407-e196-4ce3-b70c-76e7b537493f
This commit is contained in:
parent
d19e6b1633
commit
91c4e7b527
6 changed files with 144 additions and 62 deletions
|
|
@ -333,6 +333,18 @@ def _verify_policy(bao_bin: str, name: str, expected: str) -> None:
|
|||
raise BuildRefused("live reused policy does not match the approved content pin")
|
||||
|
||||
|
||||
def _verify_written_policy(bao_bin: str, name: str, expected: str) -> None:
|
||||
"""Post-build check: the stored policy is the text the plan approved.
|
||||
|
||||
ops-mason-construct can write any content under workload-kv-read-*, so
|
||||
this is the detective control (MASON-IN-0005). OpenBao may drop a
|
||||
trailing newline.
|
||||
"""
|
||||
content = _run(bao_bin, ["policy", "read", name])
|
||||
if content.rstrip("\n") != expected.rstrip("\n"):
|
||||
raise BuildError(f"written policy {name} does not match the approved text; build not recorded")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _delivery_files(path: Path):
|
||||
"""Reserve private files before Bao writes, without following any symlink.
|
||||
|
|
@ -391,33 +403,64 @@ def _write_credential(fd: int, value: str) -> None:
|
|||
|
||||
|
||||
def build_approle_kv_lane(plan: ConstructionPlan, spec: AppRoleKVSpec) -> dict[str, str]:
|
||||
"""Create the policy + AppRole for an `openbao-approle-kv` plan, deliver role_id/secret_id.
|
||||
"""Create the policy + AppRole for an `openbao-approle-kv` plan. Structure only.
|
||||
|
||||
Refuses unless plan.is_approved(). Returns object names only (no
|
||||
secret material) and appends a metadata-only audit record.
|
||||
Refuses unless plan.is_approved(). Never reads a role_id or mints a
|
||||
secret_id: ops-mason-construct denies both (MASON-IN-0005), so delivery
|
||||
is the founder's step, `deliver_approle_credentials` under a founder or
|
||||
applier session. Returns object names only and appends a metadata-only
|
||||
audit record.
|
||||
"""
|
||||
spec = replace(spec)
|
||||
plan = _approved_build(plan, spec)
|
||||
if spec.reuse_policy:
|
||||
_verify_policy(spec.bao_bin, spec.policy_name, spec.reuse_policy_sha256)
|
||||
else:
|
||||
policy_hcl = _policy_hcl(spec.kv_path, spec.kv_capabilities)
|
||||
_run(spec.bao_bin, ["policy", "write", spec.policy_name, "-"], input_text=policy_hcl)
|
||||
_verify_written_policy(spec.bao_bin, spec.policy_name, policy_hcl)
|
||||
|
||||
_run(
|
||||
spec.bao_bin,
|
||||
[
|
||||
"write",
|
||||
f"auth/approle/role/{spec.approle_name}",
|
||||
f"token_policies={spec.policy_name}",
|
||||
f"token_ttl={spec.token_ttl}",
|
||||
f"token_max_ttl={spec.token_max_ttl}",
|
||||
f"token_num_uses={spec.token_num_uses}",
|
||||
f"secret_id_ttl={spec.secret_id_ttl}",
|
||||
],
|
||||
)
|
||||
|
||||
objects = {
|
||||
"approved_spec_sha256": plan.approved_spec_sha256,
|
||||
"policy_name": spec.policy_name,
|
||||
"approle_name": spec.approle_name,
|
||||
"kv_path": spec.kv_path,
|
||||
"credential_delivery": f"founder: deliver_approle_credentials to {spec.delivery_dir}",
|
||||
}
|
||||
record_build(
|
||||
plan_id=plan.id,
|
||||
plan_path=str(plan.path),
|
||||
approved_by=plan.approved_by or "",
|
||||
approved_at=plan.approved_at or "",
|
||||
objects=objects,
|
||||
log_path=spec.audit_log_path,
|
||||
)
|
||||
return objects
|
||||
|
||||
|
||||
def deliver_approle_credentials(plan: ConstructionPlan, spec: AppRoleKVSpec) -> dict[str, str]:
|
||||
"""Deliver role_id/secret_id for an approved, built AppRole lane, write-only.
|
||||
|
||||
Not ops-mason's act: ops-mason-construct denies role-id and secret-id, so
|
||||
this runs only under a founder or applier session (MASON-IN-0005). Values
|
||||
go to private files and are never returned or logged.
|
||||
"""
|
||||
spec = replace(spec)
|
||||
plan = _approved_build(plan, spec)
|
||||
with _delivery_files(spec.delivery_dir) as files:
|
||||
if not spec.reuse_policy:
|
||||
policy_hcl = _policy_hcl(spec.kv_path, spec.kv_capabilities)
|
||||
_run(spec.bao_bin, ["policy", "write", spec.policy_name, "-"], input_text=policy_hcl)
|
||||
|
||||
_run(
|
||||
spec.bao_bin,
|
||||
[
|
||||
"write",
|
||||
f"auth/approle/role/{spec.approle_name}",
|
||||
f"token_policies={spec.policy_name}",
|
||||
f"token_ttl={spec.token_ttl}",
|
||||
f"token_max_ttl={spec.token_max_ttl}",
|
||||
f"token_num_uses={spec.token_num_uses}",
|
||||
f"secret_id_ttl={spec.secret_id_ttl}",
|
||||
],
|
||||
)
|
||||
|
||||
role_id = _run(
|
||||
spec.bao_bin, ["read", "-field=role_id", f"auth/approle/role/{spec.approle_name}/role-id"]
|
||||
).strip()
|
||||
|
|
@ -428,14 +471,11 @@ def build_approle_kv_lane(plan: ConstructionPlan, spec: AppRoleKVSpec) -> dict[s
|
|||
|
||||
_write_credential(files["role_id"], role_id)
|
||||
_write_credential(files["secret_id"], secret_id)
|
||||
delivered_to = str(spec.delivery_dir)
|
||||
|
||||
objects = {
|
||||
"approved_spec_sha256": plan.approved_spec_sha256,
|
||||
"policy_name": spec.policy_name,
|
||||
"approle_name": spec.approle_name,
|
||||
"kv_path": spec.kv_path,
|
||||
"delivered_to": delivered_to,
|
||||
"delivered_to": str(spec.delivery_dir),
|
||||
}
|
||||
record_build(
|
||||
plan_id=plan.id,
|
||||
|
|
@ -463,8 +503,8 @@ def build_kubernetes_kv_lane(
|
|||
f"auth/kubernetes/role/{spec.role_name}",
|
||||
f"bound_service_account_names={','.join(spec.service_account_names)}",
|
||||
f"bound_service_account_namespaces={','.join(spec.service_account_namespaces)}",
|
||||
f"policies={spec.policy_name}",
|
||||
f"ttl={spec.token_ttl}",
|
||||
f"token_policies={spec.policy_name}",
|
||||
f"token_ttl={spec.token_ttl}",
|
||||
],
|
||||
)
|
||||
objects = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue