Commit graph

5 commits

Author SHA1 Message Date
024993f18a Close T05: live matrix 12 pass / 0 fail / 3 skip
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
In-pod remote run. S08 needed UPDATE on secret_findings; granted and
recorded as migration 0005. Disruption scenarios remain operator-owned.
2026-08-13 12:40:05 +02:00
53df641866 Own migrate objects as audit_core_migrate; pin snapshot-read image
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
SET ROLE during migrate so tables are not owned by a disposable lease.
Current image sha256:aec5575. T02/T03 recorded done after live rotation
and rollout undo. T05 remote run is partial (port-forward dropped).
2026-08-13 12:36:25 +02:00
7636e83dcc 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>
2026-08-12 01:36:28 +02:00
fc48378a3f Accept a brokered libpq environment as connection information
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
AUDIT-WP-0005-T02. The rapp-postgres credential playbook has the
railiance-platform broker inject PGUSER/PGPASSWORD/PGHOST/PGPORT/PGDATABASE
into the child process. audit-core only accepted AUDIT_CORE_DATABASE_URL, so
consuming a brokered lease would have meant assembling a DSN by hand from the
injected variables - putting the credential back into audit-core's own
configuration, which is what the lane exists to avoid.

An empty conninfo lets libpq read those variables directly, so a brokered lease
now needs no DSN at all. AUDIT_CORE_DATABASE_URL still works for local and test
use. Missing both is a clear startup error naming each option.

Tests 80 -> 82.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:24:25 +02:00
bd274f6269 Add the PostgreSQL audit backend and a shared conformance suite
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
AUDIT-WP-0005-T01, built and verified against PostgreSQL 16 locally in
Docker; the Railiance cluster was not needed.

tests/test_backend_conformance.py is one suite run against every backend, so
"the Postgres backend is done" means it satisfies the same contract SQLite
already does rather than having its own green tests. It skips cleanly with no
server reachable; make pg-test-up and make test-pg run it. Suite 50 -> 71.

RetentionPolicy declares immutable=True and earns it: migration 0002 installs
a trigger rejecting UPDATE and DELETE on the events table, so a leaked runtime
credential can append but cannot rewrite or erase the trail. That materially
narrows the residual risk ADR-0001 section 5 called out. tamper_evidence stays
False because nothing here would prove a database owner had dropped the
trigger - hash-chaining or external anchoring would be needed and is not
implemented.

Idempotency is one statement (INSERT ... ON CONFLICT DO NOTHING RETURNING),
verified to behave identically to the SQLite backend under 12 concurrent
submissions of the same event. Migrations are ordered, recorded and
idempotent. Replay reconciles rather than duplicating - the piece deferred out
of WP-0004-T05 - and is tested to leave exactly one custody record.

Backend selection is by AUDIT_CORE_DATABASE_URL; the SQLite fallback logs a
warning so a deployment that lost its URL is visible rather than quietly
running on the wrong store.

Also fixed: ingestion had no __main__ guard, so python -m audit_core.ingestion
silently did nothing. Found during end-to-end smoke.

Counting semantics documented: occurrences counts transmissions, not stored
events, so a retry of a secret-shaped field increments it again. That is the
sender behaviour being optimized away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:09:46 +02:00