Back to Blog
How-To 10 min read

SOC 2 Controls for PostgreSQL: Database Security That Passes Audits

Configure PostgreSQL for SOC 2 compliance — covering role-based access control, row-level security, audit logging with pgaudit, encryption at rest, and connection security.

Key Takeaways
  • Create a least-privilege role per service — never connect to PostgreSQL as the superuser from application code.
  • Enable pgaudit to log all DDL, DML, and authentication events to a tamper-resistant log stream.
  • Use row-level security (RLS) policies to enforce tenant isolation at the database layer for multi-tenant SaaS products.
  • Enable SSL/TLS for all connections and reject unencrypted connections with `hostssl` rules in pg_hba.conf.
  • Encrypt the data volume at the infrastructure layer (EBS encryption, RDS storage encryption) — PostgreSQL itself does not provide native column-level encryption for most use cases.

Why PostgreSQL needs explicit SOC 2 controls

PostgreSQL is the most common database in SOC 2 audits for SaaS companies. Auditors consistently probe three areas: who can access what data (CC6), whether mutations are logged (CC7), and whether data is protected in transit and at rest (CC6.7).

Default PostgreSQL installations are permissive: the postgres superuser can do anything, all objects in the public schema are writable by any authenticated user, and logging is minimal. Each of these defaults is an audit finding waiting to happen.

This guide covers the five configuration areas that appear in virtually every PostgreSQL SOC 2 audit, with specific postgresql.conf and SQL commands for each.

Role-based access control

Create a dedicated role for each application service with only the permissions it needs: `CREATE ROLE api_service LOGIN PASSWORD '...'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO api_service; REVOKE DELETE ON sensitive_table FROM api_service;`. Never use the postgres superuser for application connections.

Use role inheritance for shared permission sets: `CREATE ROLE read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; CREATE ROLE analyst LOGIN PASSWORD '...'; GRANT read_only TO analyst;`. This makes permission reviews easier — auditors can review roles at the role level rather than per-user.

Run `SELECT grantee, table_name, privilege_type FROM information_schema.role_table_grants WHERE grantee NOT IN ('postgres', 'PUBLIC')` quarterly and document the output as access control evidence for CC6.2.

Row-level security for tenant isolation

Enable RLS on all tenant-scoped tables: `ALTER TABLE orders ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.current_tenant')::uuid);`. Set the tenant context at connection time: `SET app.current_tenant = '<uuid>';` in your connection pool setup.

RLS policies are enforced even for table owners by default when `FORCE ROW LEVEL SECURITY` is set. Add this to critical tables to prevent accidental cross-tenant queries during maintenance: `ALTER TABLE orders FORCE ROW LEVEL SECURITY;`.

Test RLS policies with separate test roles for each tenant and verify that cross-tenant queries return zero rows. Include RLS policy definitions in your system description. Auditors increasingly ask to see RLS as the primary control for multi-tenant data isolation.

Audit logging with pgaudit

Install and enable the pgaudit extension: `CREATE EXTENSION pgaudit; ALTER SYSTEM SET pgaudit.log = 'ddl, write, role';` then reload with `SELECT pg_reload_conf();`. pgaudit writes structured entries to the PostgreSQL log for every DDL statement, write operation (INSERT/UPDATE/DELETE), and role change.

Ship PostgreSQL logs to a centralised log aggregator (CloudWatch Logs, Datadog, Splunk) with a 12-month retention policy. Use the `log_destination = 'csvlog'` setting for structured output that log aggregators can parse without regex.

For RDS PostgreSQL, enable the pgaudit parameter group setting and enable Enhanced Monitoring. RDS exports logs to CloudWatch automatically. Set a CloudWatch log retention policy on the `/aws/rds/instance/*/postgresql` log group to 365 days.

TLS and connection security

Edit pg_hba.conf to replace all `host` entries with `hostssl` entries. This rejects unencrypted connections: `hostssl all all 0.0.0.0/0 scram-sha-256`. Restart PostgreSQL after changing pg_hba.conf.

Set `ssl = on` and `ssl_min_protocol_version = TLSv1.2` in postgresql.conf. Provide a valid TLS certificate signed by your internal CA or a public CA. For RDS, SSL is enabled by default — enforce it with a parameter group setting `rds.force_ssl = 1`.

Verify SSL is enforced: `SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid();` from an application connection. All application connections should show `ssl = t`. Screenshot this query result as transport security evidence (CC6.7).

Encryption at rest

Enable EBS volume encryption when creating your EC2-hosted PostgreSQL instance, or enable RDS storage encryption at database creation time. Both use AES-256 encryption with KMS-managed keys. Note: RDS storage encryption cannot be enabled after creation — plan this before launch.

For column-level encryption of specific highly sensitive fields (SSNs, payment card data), use the pgcrypto extension: `UPDATE users SET ssn_encrypted = pgp_sym_encrypt(ssn, current_setting('app.encryption_key')); ALTER TABLE users DROP COLUMN ssn;`. The encryption key should come from AWS KMS via your application layer, not stored in PostgreSQL settings.

Document your encryption posture: infrastructure-level encryption covers all data at rest. Column-level encryption is applied to PII fields as defined in your data classification policy. This two-layer approach satisfies most SOC 2 auditor expectations for encryption at rest under CC6.1.

PostgreSQL SOC 2 checklist

Before your audit: (1) No application code connects as postgres superuser — each service has a dedicated least-privilege role. (2) RLS enabled on all multi-tenant tables with FORCE ROW LEVEL SECURITY. (3) pgaudit extension enabled with ddl, write, and role logging. (4) Logs shipped to centralised aggregator with 12-month retention. (5) All connections via hostssl only — no unencrypted host entries in pg_hba.conf. (6) SSL minimum protocol TLSv1.2 configured. (7) Storage encryption enabled (RDS) or EBS volume encrypted. (8) Quarterly access control review documented.

Collect evidence screenshots: SHOW ssl; SHOW ssl_min_protocol_version; SELECT extname FROM pg_extension WHERE extname = 'pgaudit'; SELECT grantee, privilege_type FROM role_table_grants. These four queries give auditors the access and configuration evidence they need.

Frequently Asked Questions

Does Amazon RDS manage SOC 2 controls on my behalf?
AWS covers infrastructure controls for RDS (physical security, host OS patching, hardware failure). You are responsible for database-level controls: user roles, RLS policies, pgaudit configuration, parameter group settings (ssl, force_ssl), and log retention. AWS provides a Shared Responsibility Model document that auditors accept as coverage for the infrastructure layer.
What is the difference between pgaudit and PostgreSQL built-in logging?
PostgreSQL built-in logging (log_statement = all) logs all statements but lacks structured metadata — you get the raw SQL text without object type, object name, or command tag in separate fields. pgaudit adds structured fields that log aggregators and SIEMs can parse and correlate. Auditors prefer pgaudit output because it is actionable without regex parsing.
Should I use connection pooling (PgBouncer) and does it affect audit logging?
PgBouncer is recommended for production. In transaction pooling mode, SET commands (like setting the tenant context for RLS) are reset between transactions. Use session pooling mode when you rely on session-level SET for RLS context. In session mode, each client gets a dedicated backend connection, so SET commands persist for the session.
How do I handle database migrations in a SOC 2-compliant way?
Run migrations as a dedicated migration role with CREATE TABLE and ALTER TABLE permissions, separate from the runtime application role. Log migrations in a migrations table (most ORMs do this). Require peer review of migration PRs in your change management process. Document rollback procedures for each migration.
Does PostgreSQL logical replication affect my audit controls?
Replication sends all DML to replica nodes. If your replica is in a different region or account, ensure it has the same access controls and encryption settings as the primary. pgaudit on the primary does not capture DML replicated to standbys — enable pgaudit on replicas independently if you need to audit replica reads.

Automate your compliance today

AuditPath runs 86+ automated checks across AWS, GitHub, Okta, and 14 more integrations. SOC 2 and DPDP Act. Free plan available.

Start for free