Implement AUDIT-WP-0006 honest operational custody.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Postgres now reports custody_class=operational with a cited 30-day
recoverable window. Join ITC-CAP operations.audit at D4, publish the
interface card, and overlay user-engine tenants [*] from Git so an
ExternalSecret refresh cannot shrink it.
This commit is contained in:
tegwick 2026-08-16 00:24:33 +02:00
parent 0a3d05ff1c
commit ded432a63f
25 changed files with 832 additions and 94 deletions

View file

@ -17,6 +17,7 @@ import hmac
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterable
from audit_core.redaction import POLICIES, POLICY_REDACT
@ -113,7 +114,7 @@ class SenderRegistry:
env = env if env is not None else dict(os.environ)
raw = env.get("AUDIT_CORE_SENDERS")
if raw:
return cls(_parse_identities(raw))
return cls(_apply_scope_overlay(_parse_identities(raw), env))
legacy = (env.get("AUDIT_CORE_INGEST_TOKEN") or "").strip()
if not legacy:
@ -134,6 +135,66 @@ class SenderRegistry:
])
def _load_scope_overlay(env: dict[str, str]) -> list[dict[str, Any]]:
"""Non-secret sender policy. Tokens never come from here.
``AUDIT_CORE_SENDERS_SCOPE`` is inline JSON (tests).
``AUDIT_CORE_SENDERS_SCOPE_PATH`` is a file (production ConfigMap).
"""
inline = env.get("AUDIT_CORE_SENDERS_SCOPE")
if inline:
payload = json.loads(inline)
else:
path = env.get("AUDIT_CORE_SENDERS_SCOPE_PATH")
if not path:
return []
payload = json.loads(Path(path).read_text())
if not isinstance(payload, list):
raise ValueError("sender scope overlay must be a JSON list")
return [entry for entry in payload if isinstance(entry, dict) and entry.get("name")]
def _apply_scope_overlay(
identities: list[SenderIdentity], env: dict[str, str]
) -> list[SenderIdentity]:
"""Overlay non-secret fields from Git/ConfigMap onto Secret-backed tokens.
ExternalSecret refresh cannot shrink ``user-engine`` tenants below the
declared scope (AUDIT-WP-0006-T05). Tokens are never taken from the overlay.
"""
overlay = {entry["name"]: entry for entry in _load_scope_overlay(env)}
if not overlay:
return identities
merged: list[SenderIdentity] = []
for identity in identities:
extra = overlay.get(identity.name)
if extra is None:
merged.append(identity)
continue
tenants = extra.get("tenants")
sources = extra.get("sources")
merged.append(
SenderIdentity(
name=identity.name,
tokens=identity.tokens,
sources=(
frozenset(str(s) for s in sources) if sources else identity.sources
),
tenants=(
frozenset(str(t) for t in tenants) if tenants else identity.tenants
),
may_write=(
bool(extra["may_write"]) if "may_write" in extra else identity.may_write
),
may_read=(
bool(extra["may_read"]) if "may_read" in extra else identity.may_read
),
secret_policy=identity.secret_policy,
)
)
return merged
def _parse_identities(raw: str) -> list[SenderIdentity]:
try:
entries = json.loads(raw)