diff --git a/plans/audit-core-openbao-runtime-custody.md b/plans/audit-core-openbao-runtime-custody.md index 2c32185..edb9bd1 100644 --- a/plans/audit-core-openbao-runtime-custody.md +++ b/plans/audit-core-openbao-runtime-custody.md @@ -3,9 +3,9 @@ id: audit-core-openbao-runtime-custody demand_source: audit-core/workplans/AUDIT-WP-0005-T02 consumer_repo: audit-core credential_type: openbao-approle-kv -status: reviewed -approved_by: null -approved_at: null +status: built +approved_by: "Bernd Worsch" +approved_at: "2026-08-13" created: "2026-08-13" updated: "2026-08-13" --- @@ -118,3 +118,29 @@ is on this identity. **Decision:** approve, reject, or send back to phase 1. Approval authorizes Mason build of structure and in-cluster sender minting. It does not authorize printing any secret value. + +## 6. Build result + +Built 2026-08-13 after founder approve, via +`ops_mason.executor.build_approle_kv_lane` with `reuse_policy=True` so +the existing `external-secrets-audit-core` policy (database creds + +senders read) was not rewritten. + +- reused policy `external-secrets-audit-core`; +- created AppRole `external-secrets-audit-core` (15m / 30m / 8 uses); +- delivered role-id/secret-id into Secret + `external-secrets/openbao-audit-core-approle`; local delivery dir shredded; +- switched ClusterSecretStores `openbao-audit-core` and + `openbao-audit-core-database` to AppRole auth; both `Valid`; +- ExternalSecrets remain `SecretSynced`; receiver `/readyz` still + `custody_class=archive`; +- deleted interim Secret `external-secrets/openbao-audit-core-eso-token`; +- catalog entry `audit-core-senders` already drafted in ops-warden + (`warden route find "audit-core senders" --all`). + +Empty KV path `platform/workloads/audit-core/senders` was not written: +mason does not put values, and senders already live in-cluster. A later +wrap-migrate can create that path without a founder paste. + +No secret value was printed, committed, or logged. The 15-minute +platform-admin child used for the build was revoked. diff --git a/src/ops_mason/executor.py b/src/ops_mason/executor.py index 622ba1e..6f1208f 100644 --- a/src/ops_mason/executor.py +++ b/src/ops_mason/executor.py @@ -53,6 +53,9 @@ class AppRoleKVSpec: delivery_dir: Path | None = None bao_bin: str = "bao" audit_log_path: Path | None = None + # When True the named policy already exists (and may grant more than one + # KV path). Do not rewrite it — AppRole bind only. + reuse_policy: bool = False @dataclass @@ -111,8 +114,9 @@ def build_approle_kv_lane(plan: ConstructionPlan, spec: AppRoleKVSpec) -> dict[s f"approved_at={plan.approved_at!r}) — refusing to build" ) - policy_hcl = _policy_hcl(spec.kv_path, spec.kv_capabilities) - _run(spec.bao_bin, ["policy", "write", spec.policy_name, "-"], input_text=policy_hcl) + 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, diff --git a/tests/test_executor.py b/tests/test_executor.py index e4c5fea..8cd09d5 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -117,6 +117,29 @@ def test_build_writes_policy_approle_and_delivers_credentials(tmp_path) -> None: assert "secret-id-value" not in audit_text +def test_reuse_policy_does_not_rewrite_existing_policy(tmp_path) -> None: + plan = _plan(tmp_path) + spec = _spec(tmp_path) + spec.reuse_policy = True + + def fake_run(cmd, input=None, capture_output=True, text=True, timeout=30): + result = MagicMock(returncode=0, stderr="") + if cmd[1:3] == ["read", "-field=role_id"]: + result.stdout = "role-id-value\n" + elif "-field=secret_id" in cmd: + result.stdout = "secret-id-value\n" + else: + result.stdout = "" + return result + + with patch("ops_mason.executor.subprocess.run", side_effect=fake_run) as run: + build_approle_kv_lane(plan, spec) + + bao_cmds = [c.args[0] for c in run.call_args_list] + assert not any(cmd[:2] == ["bao", "policy"] for cmd in bao_cmds) + assert any(cmd[1:3] == ["write", "auth/approle/role/test-lane"] for cmd in bao_cmds) + + def test_build_appends_audit_record(tmp_path) -> None: plan = _plan(tmp_path) spec = _spec(tmp_path)