pgaudit, log rotation, and a full root disk

jun 5 2026 · 1 min read

Enabling pgaudit on the homelab control plane's Postgres cluster seemed straightforward. The policy was layered by actor: separate rules for the superuser, the app role, and the read-only role, so the audit trail would capture meaningful operations without logging every internal background query.

The disk fill alert fired within hours.

Two bugs compounded. The first was that the initial policy was too broad. Normal app traffic generated far more log volume than expected, and the audit log was accumulating faster than the configured retention window could clear it. That alone was manageable with a tighter policy.

The second bug was more subtle. The logrotate configuration was using copytruncate mode. That mode copies the current log file to a timestamped backup and then truncates the original in place. The problem is that Postgres holds the log file descriptor open across the truncation. It keeps writing to the same inode, so the truncated bytes are not returned to free space while Postgres is running. The correct approach is to signal Postgres to close and reopen its log file after rotation, which is what the postrotate directive with a pg_ctl reload (or equivalent) handles.

The resolution was two changes: rebuild the audit policy with tighter per-role scope to bring volume under control, and reconfigure logrotate to use the signal-based rotation path instead of copytruncate. Retention thresholds and disk capacity alerts were documented in the runbook after the fact.

The lesson is that log rotation needs to be tested against a running database, not a stopped one. The truncation behavior only manifests when the process holds the file descriptor, which means a pre-start test or a manual truncation will look correct while the production failure mode stays hidden until the disk fills.