Deliver database credentials as a rotatable mounted directory
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

AUDIT-WP-0005-T02 (progress). rapp-postgres has landed platform-pg with the
audit_core database, roles, and dynamic credential provisioning, so
audit-core's side is now built against it.

In-cluster delivery is a mounted directory rather than environment variables.
A dynamic lease rotates while the pod runs and an env var is fixed at process
start, so env delivery would force a restart on every rotation - and every
restart is a delivery gap, which is what this task forbids.
CredentialDirectory is re-read on every connection attempt via psycopg_pool's
callable kwargs, so a rotated lease takes effect with no restart. Rotation is
logged by password fingerprint, never by value.

deploy/externalsecrets.yaml follows the ClusterSecretStore -> ExternalSecret ->
Secret pattern already used by activity-core and rapp-qonto, at a 15m refresh
rather than the default 1h since the interval bounds how long a revoked lease
can stay mounted. All manifests validated --dry-run=server --validate=strict.

The rotation test was initially vacuous: it passed against a deliberately naive
implementation that read credentials once at startup, because pooled sessions
stay authenticated after a password change and nothing forced a reconnect. It
now terminates the role's sessions first, and is verified to fail against the
naive implementation and pass against the real one. Tests 82 -> 84.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-12 01:36:28 +02:00
parent fc48378a3f
commit 7636e83dcc
7 changed files with 364 additions and 20 deletions

View file

@ -480,17 +480,19 @@ def build_backend() -> IdempotentAuditBackend:
the wrong store.
"""
url = os.environ.get("AUDIT_CORE_DATABASE_URL")
credential_dir = os.environ.get("AUDIT_CORE_CREDENTIAL_DIR")
brokered = bool(os.environ.get("PGHOST") and os.environ.get("PGUSER"))
if url or brokered:
if url or brokered or credential_dir:
from audit_core.postgres_backend import PostgresAuditBackend
retention = os.environ.get("AUDIT_CORE_RETENTION_DAYS")
log.info(
"custody backend: postgresql (%s)",
"AUDIT_CORE_DATABASE_URL" if url else "brokered libpq environment",
)
source = ("mounted credential directory" if credential_dir
else "AUDIT_CORE_DATABASE_URL" if url
else "brokered libpq environment")
log.info("custody backend: postgresql (%s)", source)
return PostgresAuditBackend(
url or "",
credential_dir=credential_dir,
schema=os.environ.get("AUDIT_CORE_DATABASE_SCHEMA", "audit_core"),
retention_days=int(retention) if retention else None,
max_size=int(os.environ.get("AUDIT_CORE_DB_POOL_MAX", "8")),