PostgreSQL Audit Logging

PostgreSQL does not have a built-in object model that matches SQL Server Audit’s server-audit and specification hierarchy. The nearest core equivalent is the logging subsystem: choose a log destination, shape the line prefix so identity is visible, decide which connection or statement classes should be logged, and then ship that log stream into durable storage and external analysis. Richer object-class auditing usually comes from extensions such as pgaudit, which are outside core PostgreSQL and were not present in this lab.


flowchart TD
  CORE["Core PostgreSQL logging"] --> ID["log_line_prefix"]
  CORE --> SEL["log_connections / log_disconnections / log_statement / log_min_duration_statement"]
  CORE --> DEST["stderr / logging_collector / syslog"]
  DEST --> SHIP["Docker logs / file shipper / SIEM / object storage"]
  EXT["Optional extension"] --> PGA["pgaudit"]
  PGA --> DEST

Audit Architecture

PostgreSQL | built-in logging versus pgaudit | know the native boundary

Verify what exists in the current lab

The lab confirmed that pgaudit is not available:

SELECT name, default_version, installed_version, comment
FROM pg_available_extensions
WHERE name IN ('pgaudit')
ORDER BY name;
namedefault_versioninstalled_versioncomment
(0 rows)

This matters operationally:

SurfaceAvailable in this lab?Meaning
Core logging settingsYescan capture connections, disconnections, statements, durations, and server events
pgauditNoricher audit classes are not available unless the package is installed and preloaded
SQL Server-style audit object hierarchyNo direct equivalentPostgreSQL relies on log configuration and external handling instead

Build And Verify The Logging Surface

PostgreSQL | pg_settings | inspect the current logging posture

Read the baseline before changing anything

SELECT name, setting, source
FROM pg_settings
WHERE name IN (
  'logging_collector',
  'log_destination',
  'log_directory',
  'log_filename',
  'log_connections',
  'log_disconnections',
  'log_statement',
  'log_line_prefix',
  'shared_preload_libraries'
)
ORDER BY name;
namesettingsource
log_connectionsoffdefault
log_destinationstderrdefault
log_directorylogdefault
log_disconnectionsoffdefault
log_filenamepostgresql-%Y-%m-%d_%H%M%S.logdefault
log_line_prefix%m [%p]default
log_statementnonedefault
logging_collectoroffdefault
shared_preload_librariesdefault

The baseline tells the real story:

SettingOperational implication
logging_collector = offlogs are not being rotated into PostgreSQL-managed files
log_destination = stderrthe live evidence stream is the container runtime log
log_connections = off and log_disconnections = offconnection lifecycle is invisible by default
log_statement = noneDDL and DML are not logged by default
log_line_prefix = %m [%p]timestamp and PID exist, but user, database, and application name do not

PostgreSQL | minimal audit-style settings | enable identity-rich logging

Turn on connection, disconnection, and DDL visibility

For the lab, PostgreSQL was configured with:

ALTER SYSTEM SET log_connections = 'on';
ALTER SYSTEM SET log_disconnections = 'on';
ALTER SYSTEM SET log_statement = 'ddl';
ALTER SYSTEM SET log_line_prefix = '%m [%p] %u@%d %a ';
SELECT pg_reload_conf();
SELECT name, setting, source
FROM pg_settings
WHERE name IN ('log_connections','log_disconnections','log_statement','log_line_prefix')
ORDER BY name;
namesettingsource
log_connectionsonconfiguration file
log_disconnectionsonconfiguration file
log_line_prefix%m [%p] %u@%d %a configuration file
log_statementddlconfiguration file

One real operational footgun surfaced during setup: postgresql.auto.conf had briefly been left owned by root from an earlier lab cleanup, which caused ALTER SYSTEM to fail with Permission denied until ownership was returned to postgres. That is a useful reminder that audit and logging changes depend on filesystem hygiene, not only SQL privileges.


Read And Triage The Log Stream

PostgreSQL | connection, DDL, and failure evidence | read the relevant log lines back

Generate note-scoped events and inspect them

The lab generated three event classes:

application_nameAction
note12_oksuccessful connection and disconnection
note12_ddlCREATE TABLE followed by DROP TABLE
note12_failrepeated failed login attempts for nonexistent role no_such_role

Filtered log lines from the live server stream:

2026-04-18 23:47:31.151 UTC [614] postgres@stoxx [unknown] LOG:  connection authorized: user=postgres database=stoxx application_name=note12_ddl
2026-04-18 23:47:31.152 UTC [614] postgres@stoxx note12_ddl LOG:  statement: CREATE TABLE demo_stc.note12_audit_demo(id int); DROP TABLE demo_stc.note12_audit_demo;
2026-04-18 23:47:31.154 UTC [614] postgres@stoxx note12_ddl LOG:  disconnection: session time: 0:00:00.003 user=postgres database=stoxx host=127.0.0.1 port=42948
2026-04-18 23:47:31.158 UTC [615] postgres@stoxx [unknown] LOG:  connection authorized: user=postgres database=stoxx application_name=note12_ok
2026-04-18 23:47:31.159 UTC [615] postgres@stoxx note12_ok LOG:  disconnection: session time: 0:00:00.001 user=postgres database=stoxx host=127.0.0.1 port=42964
2026-04-18 23:47:31.208 UTC [625] no_such_role@postgres [unknown] FATAL:  role "no_such_role" does not exist
2026-04-18 23:47:31.223 UTC [627] no_such_role@postgres [unknown] FATAL:  role "no_such_role" does not exist
2026-04-18 23:47:31.240 UTC [629] no_such_role@postgres [unknown] FATAL:  role "no_such_role" does not exist

These lines demonstrate the three core auditing questions:

QuestionPostgreSQL evidence
Who connected?user, database, application name, and source host in the connection and disconnection lines
What DDL happened?statement: log line because log_statement = 'ddl'
Did authentication fail?FATAL lines in the server log

Detect Failed-Login Bursts

PostgreSQL | failed login triage | count repeated authentication failures

Summarize the test burst

The filtered log stream contained 6 failed-login events for no_such_role during the note-scoped test run.

Operationally:

SignalMeaning
repeated FATAL: role "..." does not existpassword spray against nonexistent principals, typoed automation, or account drift
same application_name repeatedlikely one automation path rather than unrelated users
same source host and short time windowraises the urgency if the host is not a trusted jump box or app server

This is the PostgreSQL equivalent of the SQL Server note’s failed-login burst section: the logic is the same, but the evidence source is the log stream rather than sys.fn_get_audit_file.


Operational Integration

PostgreSQL | durability boundary | understand what the current lab does not yet provide

Read the current retention posture honestly

SettingCurrent valueMeaning
logging_collectoroffPostgreSQL is not rotating its own log files
log_destinationstderrevidence leaves the engine through container stderr
log_directory / log_filenameconfigured but inactive for current flowthese matter only if logging_collector is enabled

Practical consequences:

If you need…Then…
short-term local debuggingcontainer stderr may be sufficient
durable audit evidenceenable logging_collector or ship stderr externally immediately
structured object-class auditinginstall and configure pgaudit, then ship those logs too
central alertingforward to Cloud Logging, a SIEM, or another indexed log sink

PostgreSQL | access control | separate event generation from event reading

Treat log access as a security boundary

PostgreSQL core does not create a separate “audit reader” permission model. In practice:

OperationTypical access boundary
change logging settingsPostgreSQL superuser plus filesystem control
read container stderr logsplatform or container-runtime access
read PostgreSQL log files with logging collector enabledfilesystem access to the log directory

That makes reader governance an infrastructure decision as much as a database one.


PostgreSQL Audit Logging Recommendations

Use the core logging surface for baseline auditability first: turn on identity-rich prefixes, capture connection lifecycle, and log at least DDL. If compliance or security operations require richer statement-class auditing, install pgaudit deliberately and ship the resulting logs into an external indexed system. Do not confuse “events are visible in docker logs right now” with “audit evidence is durably retained and access-controlled”.

Next: 13-postgresql-encryption-at-rest-and-in-transit moves from evidence to protection: TLS, client authentication boundaries, data-at-rest encryption realities in PostgreSQL, and the nearest equivalents to SQL Server’s encryption surface.