Deliver database credentials as a rotatable mounted directory
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:
parent
fc48378a3f
commit
7636e83dcc
7 changed files with 364 additions and 20 deletions
|
|
@ -316,3 +316,96 @@ def test_missing_connection_information_is_a_clear_error(monkeypatch):
|
|||
|
||||
with pytest.raises(ValueError, match="no connection information"):
|
||||
PostgresAuditBackend()
|
||||
|
||||
|
||||
# --- rotating credentials from a mounted directory (T02) --------------------
|
||||
|
||||
def _write_credentials(directory, *, user, password, url):
|
||||
import urllib.parse
|
||||
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
(directory / "username").write_text(user)
|
||||
(directory / "password").write_text(password)
|
||||
(directory / "host").write_text(parsed.hostname or "127.0.0.1")
|
||||
(directory / "port").write_text(str(parsed.port or 5432))
|
||||
(directory / "dbname").write_text((parsed.path or "/postgres").lstrip("/"))
|
||||
|
||||
|
||||
@pg_only
|
||||
def test_rotating_a_mounted_credential_needs_no_restart(tmp_path):
|
||||
"""The property env vars cannot provide.
|
||||
|
||||
A dynamic lease changes while the pod runs. Reading the credential at
|
||||
connection time rather than at startup is what makes that rotation
|
||||
invisible to senders — no restart, no delivery gap.
|
||||
"""
|
||||
import psycopg
|
||||
|
||||
from audit_core.postgres_backend import PostgresAuditBackend
|
||||
|
||||
schema = f"rot_{uuid.uuid4().hex[:10]}"
|
||||
role = f"lease_{uuid.uuid4().hex[:8]}"
|
||||
admin = psycopg.connect(PG_URL, autocommit=True)
|
||||
directory = tmp_path / "db"
|
||||
directory.mkdir()
|
||||
backend = None
|
||||
try:
|
||||
admin.execute(f"CREATE ROLE {role} LOGIN PASSWORD 'first' SUPERUSER")
|
||||
_write_credentials(directory, user=role, password="first", url=PG_URL)
|
||||
|
||||
backend = PostgresAuditBackend(
|
||||
"", schema=schema, credential_dir=str(directory), min_size=1, max_size=2
|
||||
)
|
||||
event = make_event("rot-1")
|
||||
assert backend.accept(event, digest(event)).duplicate is False
|
||||
|
||||
# Rotate: new password in the database, new password in the mount.
|
||||
# The process is never restarted and the object is never rebuilt.
|
||||
admin.execute(f"ALTER ROLE {role} PASSWORD 'second'")
|
||||
(directory / "password").write_text("second")
|
||||
|
||||
# Existing pooled sessions stay authenticated after a password change,
|
||||
# so they must be terminated for this to test anything at all — without
|
||||
# this the pool keeps serving on the old connections and an
|
||||
# implementation that read the credential once at startup would pass.
|
||||
admin.execute(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
|
||||
"WHERE usename = %s AND pid <> pg_backend_pid()", (role,)
|
||||
)
|
||||
backend.pool.check() # discard the dead connections and reopen
|
||||
|
||||
event2 = make_event("rot-2")
|
||||
assert backend.accept(event2, digest(event2)).duplicate is False
|
||||
assert backend.get("rot-1")["event_id"] == "rot-1"
|
||||
finally:
|
||||
if backend:
|
||||
with backend.pool.connection() as conn:
|
||||
conn.execute(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE')
|
||||
backend.close()
|
||||
admin.execute(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
|
||||
"WHERE usename = %s AND pid <> pg_backend_pid()", (role,)
|
||||
)
|
||||
admin.execute(f"DROP ROLE IF EXISTS {role}")
|
||||
admin.close()
|
||||
|
||||
|
||||
@pg_only
|
||||
def test_a_stale_mounted_credential_fails_closed(tmp_path):
|
||||
"""A revoked lease must surface as unavailable, not as silent success."""
|
||||
import psycopg
|
||||
|
||||
from audit_core.postgres_backend import PostgresAuditBackend
|
||||
|
||||
role = f"lease_{uuid.uuid4().hex[:8]}"
|
||||
admin = psycopg.connect(PG_URL, autocommit=True)
|
||||
directory = tmp_path / "db"
|
||||
directory.mkdir()
|
||||
try:
|
||||
admin.execute(f"CREATE ROLE {role} LOGIN PASSWORD 'only' SUPERUSER")
|
||||
_write_credentials(directory, user=role, password="wrong-password", url=PG_URL)
|
||||
with pytest.raises((BackendUnavailableError, Exception)):
|
||||
PostgresAuditBackend("", credential_dir=str(directory), migrate=True)
|
||||
finally:
|
||||
admin.execute(f"DROP ROLE IF EXISTS {role}")
|
||||
admin.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue