Skip to content

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 50

pgaudit 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 table
  • ROLE — GRANT, REVOKE, CREATE ROLE, ALTER ROLE, DROP ROLE
  • DDL — 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 desc

DDL 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 desc

ROLE 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 desc

High-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 desc

Baseline 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, Message

TRUNCATE 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, Message

Cross-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, Message

Any 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, Message

DROP 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:

  1. Get the timestamp and statement from the pgaudit log
  2. 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
  3. The Container App logs the API endpoint and user that triggered each DB write

Responding to Anomalies

TRUNCATE or DROP outside CM window

  1. Open a SecOps incident with severity HIGH
  2. 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)
  3. Check Azure Database for PostgreSQL activity logs: Azure Portal → psql-secops-prod → Activity log
  4. 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
  5. If unauthorized: rotate the DB password immediately, revoke the offending session, notify Rory

Unexpected privilege escalation

  1. Identify which role received the grant
  2. Check if the role exists and what it can access: \dp in psql to show table-level permissions
  3. Revoke if unauthorized: REVOKE <privilege> ON <table> FROM <role>;
  4. Rotate credentials if the escalation suggests account compromise

Bulk INSERT/DELETE anomaly

  1. Determine the volume and tables involved
  2. Is this a legitimate bulk operation (agent run, migration)? Check Container Apps logs and CM tickets
  3. 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 asc

Export 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).


  • runbooks/pgaudit-setup.md — enabling pgaudit on psql-secops-prod
  • runbooks/postgresql-operations.md — general PostgreSQL operations
  • runbooks/log-retention-verification.md — verifying 6-year retention
  • compliance/hipaa-controls.md — §164.312(b) audit control mapping

Internal use only — Cirius Group