diff --git a/audit_core/postgres_backend.py b/audit_core/postgres_backend.py index 4eb10e0..ec775e7 100644 --- a/audit_core/postgres_backend.py +++ b/audit_core/postgres_backend.py @@ -204,6 +204,13 @@ class PostgresAuditBackend: applied: list[str] = [] try: with self.pool.connection() as conn: + migrate_role = os.environ.get("AUDIT_CORE_MIGRATE_ROLE", "").strip() + if migrate_role: + if not migrate_role.isidentifier(): + raise ValueError(f"unsafe migrate role: {migrate_role!r}") + # Leased login roles must SET ROLE to the group so new + # objects are owned by audit_core_migrate, not the lease. + conn.execute(f'SET ROLE "{migrate_role}"') conn.execute(f'CREATE SCHEMA IF NOT EXISTS "{self.schema}"') conn.execute( f'CREATE TABLE IF NOT EXISTS "{self.schema}".schema_migrations (' diff --git a/deploy/audit-core.yaml b/deploy/audit-core.yaml index d6402fb..3dbc588 100644 --- a/deploy/audit-core.yaml +++ b/deploy/audit-core.yaml @@ -80,7 +80,7 @@ spec: - name: audit-core # REPLACE at release time with the built digest. A mutable tag is not # an immutable image, and `:latest` must never be the only reference. - image: forgejo.coulomb.social/coulomb/audit-core@sha256:41493cd5bbd86a3309af07b1a97d41343b91d758a300ad39d71e4a18c17de7cc + image: forgejo.coulomb.social/coulomb/audit-core@sha256:aec5575ceadf1dc53d0892c0786f1fcd5cb38077ce36ba271ee7204aacd80df5 imagePullPolicy: IfNotPresent ports: - name: http diff --git a/deploy/migrate-job.yaml b/deploy/migrate-job.yaml index 2737c74..53ed674 100644 --- a/deploy/migrate-job.yaml +++ b/deploy/migrate-job.yaml @@ -33,7 +33,7 @@ spec: type: RuntimeDefault containers: - name: migrate - image: forgejo.coulomb.social/coulomb/audit-core@sha256:41493cd5bbd86a3309af07b1a97d41343b91d758a300ad39d71e4a18c17de7cc + image: forgejo.coulomb.social/coulomb/audit-core@sha256:aec5575ceadf1dc53d0892c0786f1fcd5cb38077ce36ba271ee7204aacd80df5 imagePullPolicy: IfNotPresent command: ["python", "-m", "audit_core", "migrate"] env: @@ -41,6 +41,8 @@ spec: value: /etc/audit-core/db - name: AUDIT_CORE_DATABASE_SCHEMA value: audit_core + - name: AUDIT_CORE_MIGRATE_ROLE + value: audit_core_migrate resources: requests: cpu: 25m diff --git a/workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md b/workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md index 83d8343..0a358fa 100644 --- a/workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md +++ b/workplans/AUDIT-WP-0005-postgres-store-and-production-deployment.md @@ -115,7 +115,7 @@ cluster and belongs to T05. ```task id: AUDIT-WP-0005-T02 -status: progress +status: done priority: high state_hub_task_id: "831b2472-0d80-4369-a5e3-eb08ef3526b1" ``` @@ -218,15 +218,29 @@ Mason plan approved 2026-08-13 and built: AppRole ClusterSecretStores on AppRole and `Valid`. Interim ESO token Secret removed. Receiver stayed Ready. -Remaining before T02 done: a live rotation drill (refresh lease, confirm -no delivery gap). Empty senders KV path is a later wrap-migrate, not a -founder paste. +Live rotation 2026-08-13: ESO refresh while Ready; accept 202 before and +after; both events readable; `/readyz` stayed `archive`. Fingerprint log +was not seen on that refresh (lease may have been reused). + +Findings fixed on the way: + +- ESO AppRole login+discard **revokes** `database/creds` leases. Working + client is a renewable orphan token. AppRole remains for a later + lease-aware generator. +- Mounted Secret reads must use Kubernetes `..data` or they can tear + across two leases. +- Migrate Job created tables owned by the ephemeral login role; catch-up + GRANT + `REASSIGN OWNED` to `audit_core_migrate`. `SET ROLE + audit_core_migrate` is now in migrate(). + +T02 done for the provisioned-lane + no-delivery-gap claim. Empty senders +KV wrap-migrate is follow-on, not a founder paste. ## T03 - Deploy the receiver ```task id: AUDIT-WP-0005-T03 -status: progress +status: done priority: high state_hub_task_id: "598af2ac-e772-4a4e-9a65-dde9d4ca167f" ``` @@ -321,8 +335,11 @@ rapp-postgres isolation-test stub `events` table (3 columns, 2 fixture rows — not audit custody). Secret mounts use `fsGroup: 10001` and mode `0440` (0400 is unreadable as uid 10001). -Remaining before T03 done: pod delete/reschedule and `kubectl rollout undo` -recorded as a tested rollback. +Tested 2026-08-13: pod delete/recreate stayed Ready; `kubectl rollout undo` +returned to `sha256:41493cd5…` Ready; undo again returned to +`sha256:aec5575…` (snapshot-read image) Ready. Current pin is +`forgejo.coulomb.social/coulomb/audit-core@sha256:aec5575ceadf1dc53d0892c0786f1fcd5cb38077ce36ba271ee7204aacd80df5`. +T03 done. ## T04 - Migrate existing SQLite records @@ -440,10 +457,13 @@ against an unreachable receiver (it does not pass silently), and the emitted evidence JSON contains no tokens, credentials, or event payloads, so a run against the deployed receiver can go to NK-WP-0024 as-is. -Remaining before done: run it against the deployed receiver on railiance01 and -hand the resulting evidence to NK-WP-0024. The local rehearsal is not a -substitute — it does not exercise CNPG failover, NetworkPolicy enforcement, or -OpenBao-leased credentials. +Remote run 2026-08-13 (`evidence/failure-matrix-20260813T103540Z.json`, +`MODE=remote DISRUPT=0`): **7 passed, 5 failed, 3 skipped**. S01–S07 passed +on the live receiver (accept, duplicate, conflict, cross-tenant 400, dead +letter, 401, redaction). S08–S12 failed with `status=0` (port-forward +dropped mid-run), not a contract mismatch. S13–S15 skipped (`DISRUPT=0`, +replay needs DB). Re-run through a stable operator path before handing +NK-WP-0024; do not treat this JSON as closeout. ## T06 - Operational handover