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.
- 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.
In this guide
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?
What is the difference between pgaudit and PostgreSQL built-in logging?
Should I use connection pooling (PgBouncer) and does it affect audit logging?
How do I handle database migrations in a SOC 2-compliant way?
Does PostgreSQL logical replication affect my audit controls?
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