Appearance
pgaudit Operations — psql-secops-prod
Purpose: Day-to-day operations for pgaudit once it is enabled on psql-secops-prod — what to query, what anomalies to watch for, and how to respond.
Audience: Rory Prerequisite: pgaudit must be enabled and forwarding to LAW. See runbooks/pgaudit-setup.md for setup.
Where Logs Land
pgaudit events flow: psql-secops-prod → Azure Diagnostics → cirius-logging-law-central
LAW table: AzureDiagnosticsFilter: Category == "PostgreSQLLogs" and Message contains AUDIT:
kql
AzureDiagnostics
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:"
| project TimeGenerated, Resource, Message
| order by TimeGenerated desc
| take 50pgaudit Log Format
Each audit line in the Message field looks like:
AUDIT: SESSION,1,1,DDL,CREATE TABLE,,,CREATE TABLE foo (id int),<none>
AUDIT: SESSION,1,1,WRITE,INSERT,public,incidents,INSERT INTO incidents ...,<none>
AUDIT: SESSION,1,1,ROLE,GRANT,,,GRANT SELECT ON incidents TO readonly_user,<none>Fields: AUDIT: {type},{class_seq},{subclass_seq},{class},{command},{object_type},{object_name},{statement},{parameter}
Audit classes enabled (from pgaudit-setup.md): write,role,ddl
WRITE— INSERT, UPDATE, DELETE, TRUNCATE on any tableROLE— GRANT, REVOKE, CREATE ROLE, ALTER ROLE, DROP ROLEDDL— CREATE/ALTER/DROP TABLE, INDEX, FUNCTION, VIEW, etc.
Routine Queries
Activity in the last 24 hours
kql
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:"
| extend AuditClass = extract("AUDIT: SESSION,[0-9]+,[0-9]+,([A-Z]+),", 1, Message)
| summarize Count = count() by AuditClass, bin(TimeGenerated, 1h)
| order by TimeGenerated descDDL changes (schema modifications)
kql
AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:" and Message contains ",DDL,"
| project TimeGenerated, Message
| order by TimeGenerated descROLE changes (privilege modifications)
kql
AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:" and Message contains ",ROLE,"
| project TimeGenerated, Message
| order by TimeGenerated descHigh-volume WRITE activity (potential data exfiltration or bulk operation)
kql
AzureDiagnostics
| where TimeGenerated > ago(4h)
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:" and Message contains ",WRITE,"
| summarize WriteCount = count() by bin(TimeGenerated, 10m)
| where WriteCount > 500
| order by TimeGenerated descBaseline write volume: check normal patterns first. A sudden spike in writes (>3× normal) is worth investigating.
Anomalies to Watch For
TRUNCATE on a production table
kql
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "PostgreSQLLogs"
| where Message contains "TRUNCATE"
| project TimeGenerated, MessageTRUNCATE empties a table without row-level logging. In a production database it should never happen outside a CM window. Any TRUNCATE outside a CM window is HIGH severity — it could be data destruction.
Unexpected DDL outside CM window
kql
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "PostgreSQLLogs"
| where Message contains ",DDL,"
| project TimeGenerated, MessageCross-reference with CM tickets. DDL changes outside of Terraform applies or migrations are suspicious.
ROLE privilege escalation
kql
AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "PostgreSQLLogs"
| where Message contains ",ROLE," and (Message contains "GRANT" or Message contains "superuser")
| project TimeGenerated, MessageAny GRANT of superuser or GRANT on sensitive tables (incidents, findings, known_good_rules) outside of a planned migration is HIGH severity.
DROP TABLE or DROP DATABASE
kql
AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "PostgreSQLLogs"
| where Message contains "DROP TABLE" or Message contains "DROP DATABASE"
| project TimeGenerated, MessageDROP statements should only appear during planned migrations with a CM ticket. Any unexpected DROP is CRITICAL — potential data destruction.
Connecting the psql User to an Action
pgaudit logs the database user, not the application layer user. In the SecOps platform, all agents connect as the same service account (e.g., secops_app). To trace a database write back to an application action:
- Get the timestamp and statement from the pgaudit log
- Cross-reference with SecOps application logs in Container Apps:kql
ContainerAppConsoleLogs | where TimeGenerated between (datetime(<start>) .. datetime(<end>)) | where Log contains "POST /api/" or Log contains "INSERT" or Log contains "UPDATE" | project TimeGenerated, ContainerName, Log - The Container App logs the API endpoint and user that triggered each DB write
Responding to Anomalies
TRUNCATE or DROP outside CM window
- Open a SecOps incident with severity HIGH
- Identify who executed the statement — check which Container App instance was connected at that time, or whether the connection came from a psql session (direct DB access)
- Check Azure Database for PostgreSQL activity logs: Azure Portal → psql-secops-prod → Activity log
- If direct DB access (not via the app): check who has DB credentials. Credentials are in Key Vault — check Key Vault access logs:bash
az monitor activity-log list --resource-group rg-logging-logs \ --resource-type "Microsoft.KeyVault/vaults" --offset 24h - If unauthorized: rotate the DB password immediately, revoke the offending session, notify Rory
Unexpected privilege escalation
- Identify which role received the grant
- Check if the role exists and what it can access:
\dpin psql to show table-level permissions - Revoke if unauthorized:
REVOKE <privilege> ON <table> FROM <role>; - Rotate credentials if the escalation suggests account compromise
Bulk INSERT/DELETE anomaly
- Determine the volume and tables involved
- Is this a legitimate bulk operation (agent run, migration)? Check Container Apps logs and CM tickets
- If unexplained: check whether data was exfiltrated before the bulk DELETE — compare row counts with the last known backup
Evidence for Compliance Audits
pgaudit logs are the primary evidence for HIPAA §164.312(b) (Audit Controls) and SOC2 CC6.1 at the database layer. For audits, export:
kql
AzureDiagnostics
| where TimeGenerated between (datetime(<audit_period_start>) .. datetime(<audit_period_end>))
| where Category == "PostgreSQLLogs"
| where Message contains "AUDIT:"
| project TimeGenerated, Message
| order by TimeGenerated ascExport to CSV from LAW and provide to auditors as PostgreSQL audit log evidence. Retain 6 years per HIPAA retention requirements (LAW retention policy must be set to 6 years or logs archived to cold storage — verify in runbooks/log-retention-verification.md).
Related Documents
runbooks/pgaudit-setup.md— enabling pgaudit on psql-secops-prodrunbooks/postgresql-operations.md— general PostgreSQL operationsrunbooks/log-retention-verification.md— verifying 6-year retentioncompliance/hipaa-controls.md— §164.312(b) audit control mapping