Build audit-core AppRole lane after founder approval

reuse_policy leaves external-secrets-audit-core intact so database
leases keep working. AppRole delivered to Kubernetes; interim static
ESO token retired.
This commit is contained in:
tegwick 2026-08-13 10:42:59 +02:00
parent 93b9c1a027
commit 8e41f85121
3 changed files with 58 additions and 5 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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)