Why Hardening Matters
A default PostgreSQL installation is functional but not production-secure. SSL is disabled. Password hashing may use the legacy MD5 algorithm. Logging captures nothing by default. Every one of these gaps is an audit finding — and a potential breach vector.
This checklist covers the 14 most critical security checks for PostgreSQL, based on the CIS Benchmark and supplemented with compliance-specific validations.
The 14 Critical Checks
1. SSL Enforcement
Check: SHOW ssl; should return on.
Why: Without SSL, database credentials and query data travel in plaintext. Anyone with network access can intercept them.
-- postgresql.conf
ssl = on
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
In pg_hba.conf, replace host with hostssl to require encrypted connections:
hostssl all all 0.0.0.0/0 scram-sha-256
Severity: Critical. This is the #1 finding in PostgreSQL security audits.
2. Password Encryption Method
Check: SHOW password_encryption; should return scram-sha-256.
Why: MD5 hashing is cryptographically weak. SCRAM-SHA-256 provides proper challenge-response authentication.
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
3. Superuser Count
Check: Count roles with rolsuper = true. Should be ≤ 1.
Why: Superusers bypass all permission checks including Row-Level Security. They can read every table, modify every row, and cannot be effectively audited.
SELECT rolname FROM pg_roles WHERE rolsuper = true;
Every additional superuser beyond the bootstrap account is an audit finding.
4. Search Path Hardening
Check: SHOW search_path; should not include user-writable schemas before system schemas.
Why: A manipulated search_path can cause functions to resolve to attacker-controlled schemas (search path injection).
ALTER SYSTEM SET search_path = '"$user", public';
5. Connection Logging
Check: SHOW log_connections; should return on.
Why: Without connection logging, you cannot track who connects to your database, when, and from where.
ALTER SYSTEM SET log_connections = on;
ALTER SYSTEM SET log_disconnections = on;
6. Statement Logging
Check: SHOW log_statement; should be at least ddl.
Why: DDL logging captures all schema changes (CREATE, ALTER, DROP). Essential for change management and DORA Article 9 compliance.
ALTER SYSTEM SET log_statement = 'ddl';
For high-security environments, use all — but be aware of performance and storage implications.
7. Idle Transaction Timeout
Check: SHOW idle_in_transaction_session_timeout; should be set (not 0).
Why: Idle transactions hold locks and can cause table bloat. Set a reasonable timeout.
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
8. Row-Level Security on PII Tables
Check: Every table containing PII should have RLS enabled.
Why: Without RLS, any role with SELECT permission can read all rows. RLS ensures users only see their own data.
-- Enable RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Create policy: users see only their own data
CREATE POLICY user_isolation ON users
USING (user_id = current_setting('app.current_user_id'));
With pgcomply:
SELECT pgcomply.enable_rls('users', 'user_id');
9. Password Policy
Check: Minimum length ≥ 12, complexity enabled.
Why: PostgreSQL has no built-in password policy. pgcomply adds one:
SELECT pgcomply.set_password_policy(
min_length := 12,
require_upper := true,
require_lower := true,
require_digit := true,
require_special := true,
max_age_days := 90
);
10. Audit Trail Integrity
Check: Immutable audit log with verified hash chain.
Why: An audit trail that can be modified is worthless. pgcomply uses SHA-256 chaining where each entry's hash depends on the previous entry.
SELECT pgcomply.verify_audit();
11-14. Additional Checks
The remaining checks cover:
- Non-default port (reduce automated scanning)
- Minimum log duration (capture slow queries)
- PII coverage (all tables scanned for personal data)
- Retention policies (data doesn't accumulate indefinitely)
- Audit log immutability (DELETE/UPDATE triggers prevent tampering)
- pg_stat_statements loaded (query performance tracking)
Running All Checks at Once
Instead of checking each setting manually:
SELECT * FROM pgcomply.health_check();
Output:
rule | category | severity | status | detail
------------------+------------+----------+--------+----------------------------------
SSL_ENFORCED | encryption | critical | FAIL | ssl = off
PW_ENCRYPTION | auth | critical | PASS | scram-sha-256
SUPERUSER_COUNT | access | high | WARN | 2 superusers (recommend ≤ 1)
ROW_LEVEL_SEC | access | medium | WARN | 3 PII tables without RLS
AUDIT_INTEGRITY | pgcomply | critical | PASS | SHA-256 chain verified
...
14 checks, one function call, actionable results.
Making It Continuous
Security is not a one-time check. Schedule health_check() to run weekly:
-- With pg_cron
SELECT cron.schedule('weekly-health', '0 6 * * 1',
'SELECT pgcomply.health_check()');
Or use pgcomply's built-in scheduler:
SELECT pgcomply.schedule_jobs(install := true);
This sets up automated health checks, retention enforcement, session logging, and drift detection.
Summary
PostgreSQL security hardening requires attention to 14 critical configuration areas. The most impactful changes are enabling SSL, using SCRAM-SHA-256, minimizing superusers, and enabling Row-Level Security on PII tables. pgcomply.health_check() automates the entire assessment in a single SQL call — run it today to find out where your database stands.