$ dbaguide.com / cheat-sheets / postgresql / security

PostgreSQL Security Hardening

End-to-End Security Hardening Guide for Production Databases

PostgreSQL 15 / 16 / 17 compatible
Updated: October 2026 · Production-grade
01

Authentication pg_hba.conf

Most Critical
MethodStatusDescription
scram-sha-256RECOMMENDEDChallenge-response, salted hash. Default (PG14+).
certRECOMMENDEDClient certificate authentication (hostssl).
peerLocalUnix socket — OS user name mapping.
md5LEGACYWeak hash, replay risk. Deprecated in PG18.
passwordFORBIDDENPassword is sent in clear text.
trustFORBIDDENPasswordless login! Isolated localhost only.
Secure pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD local all postgres peer local all all scram-sha-256 hostssl app_db app_user 10.0.1.0/24 scram-sha-256 hostssl all dba_team 10.0.9.0/28 cert hostssl replication repl 10.0.2.11/32 scram-sha-256 hostnossl all all 0.0.0.0/0 reject # REMOVE ↓ host all all 0.0.0.0/0 trust
Apply & Verify
-- Apply without restart SELECT pg_reload_conf(); -- Check rules and errors SELECT line_number, type, database, user_name, address, auth_method, error FROM pg_hba_file_rules ORDER BY line_number; -- Find users with md5 hashes SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';
⚠ Warning: pg_hba.conf applies the first matching line — put restrictive rules on top. The error column must be empty before reload; otherwise the rules won't load.
02

Password Policy

Parameter / FeatureRecommended
password_encryptionHash algorithm for new passwords (postgresql.conf).scram-sha-256
passwordcheckRejects weak passwords. Loaded via shared_preload_libraries.Enabled
VALID UNTILPassword expiration date (human accounts).90 days
CREATEROLERole creation privilege — admin roles only.Restrict
LOGIN + NOINHERITService accounts don't automatically inherit member role privileges.Service acct.
auth_delay.millisecondsDelay on failed login — slows down brute-force.500
postgresql.conf
password_encryption = 'scram-sha-256' shared_preload_libraries = 'passwordcheck,auth_delay' auth_delay.milliseconds = 500 authentication_timeout = 30s
Role & Password Management
-- Human account: expiring password CREATE ROLE ali LOGIN PASSWORD 'G%7xQ!r2...' VALID UNTIL '2027-01-10'; -- Service account: no inheritance, connection limit CREATE ROLE svc_api LOGIN NOINHERIT CONNECTION LIMIT 50 PASSWORD '...'; -- Revoke CREATEROLE privilege ALTER ROLE app_admin NOCREATEROLE; -- Expired / soon-to-expire passwords SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolcanlogin AND rolvaliduntil < now() + interval '14 days';
⚠ Warning: passwordcheck can only validate passwords sent in plain text. psql \password hashes the password client-side — the check is skipped in that case.
✓ Best Practice: Since PG16, a CREATEROLE holder can only manage roles it has ADMIN OPTION on — audit the privilege chain regularly.
03

Network Security postgresql.conf

ParameterDefaultRecommended
listen_addressesListen only on required interfaces; avoid '*'.localhost'localhost,10.0.1.5'
portChanging the port isn't security, but it reduces scan noise.54325432 / custom
sslEnable TLS encryption.offon
ssl_min_protocol_versionBlock TLS 1.0/1.1 protocols.TLSv1.2TLSv1.2+
ssl_ciphersExclude weak cipher suites.HIGH:MEDIUM...HIGH:!aNULL
:!MD5:!3DES
ssl_prefer_server_ciphersUse the server's cipher order.onon
max_connectionsLimit DoS surface; use a pooler (PgBouncer).100as needed
SSL/TLS Configuration
listen_addresses = 'localhost,10.0.1.5' ssl = on ssl_cert_file = 'server.crt' ssl_key_file = 'server.key' # chmod 600 ssl_ca_file = 'root.crt' ssl_min_protocol_version = 'TLSv1.2' ssl_ciphers = 'HIGH:!aNULL:!MD5:!3DES:!RC4'
Firewall (defense in depth)
# App subnet only → 5432 ufw allow from 10.0.1.0/24 to any port 5432 proto tcp ufw deny 5432/tcp # firewalld alternative firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.1.0/24 port port=5432 protocol=tcp accept'
SSL Check on Active Connections
SELECT a.usename, a.client_addr, s.ssl, s.version, s.cipher FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid) WHERE a.backend_type = 'client backend';
✓ Best Practice: Firewall + listen_addresses + pg_hba.conf form a three-layer defense. If one is misconfigured, the others still protect you. Never expose the database directly to the internet.
04

Role & Privilege Management

Built-in Role (Predefined)Usage
pg_read_all_dataSELECT on all tables (PG14+) — reporting
pg_write_all_dataINSERT/UPDATE/DELETE on all tables (PG14+)
pg_monitorMonitoring tools — no superuser needed
pg_signal_backendCancel queries / terminate sessions
pg_checkpointRun CHECKPOINT (PG15+)
pg_maintainVACUUM, ANALYZE, REINDEX (PG17+)
Least Privilege + public Schema
-- Lock down public schema (PG15+ CREATE off by default) REVOKE ALL ON SCHEMA public FROM PUBLIC; REVOKE ALL ON DATABASE app_db FROM PUBLIC; -- Group role → grant to user CREATE ROLE app_ro NOLOGIN; GRANT CONNECT ON DATABASE app_db TO app_ro; GRANT USAGE ON SCHEMA app TO app_ro; GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_ro; ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO app_ro; GRANT app_ro TO report_user;
Superuser Audit & SET ROLE
SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolbypassrls, rolcanlogin FROM pg_roles WHERE rolsuper OR rolcreaterole OR rolbypassrls; -- Temporary privilege escalation (NOINHERIT user) SET ROLE dba_admin; -- do the work RESET ROLE;
Row-Level Security (RLS)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY; ALTER TABLE orders FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant_id')::int);
⚠ Warning: Table owners and BYPASSRLS roles bypass RLS. Use FORCE ROW LEVEL SECURITY for owners; the application should never connect as the table owner.
05

Auditing & Logging

ParameterValueDescription
log_connectionsonLog successful/failed connections
log_disconnectionsonLog disconnections with session duration
log_statement'ddl''mod' = DDL + DML write operations
log_min_duration_statement1000Slow queries over 1 s (ms)
log_lock_waitsonLock waits exceeding deadlock_timeout
log_file_mode0600Log files readable by owner only
log_parameter_max_length0Hide bind parameters (sensitive data)
Recommended log_line_prefix
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h ' # %t time · %p PID · %l line no · %u user # %d database · %a application · %h client IP
pgaudit — Detailed Audit Trail
# postgresql.conf (restart required) shared_preload_libraries = 'pgaudit' pgaudit.log = 'ddl, role, write' pgaudit.log_catalog = off pgaudit.log_relation = on pgaudit.log_parameter = off # avoid leaking sensitive data pgaudit.role = 'auditor'
CREATE EXTENSION pgaudit; -- Object-level audit: sensitive table CREATE ROLE auditor NOLOGIN; GRANT SELECT, UPDATE ON customers_pii TO auditor; -- Extra audit for a specific user only ALTER ROLE contractor SET pgaudit.log = 'read, write';
Sample Audit Log Line
AUDIT: SESSION,1,1,DDL,DROP TABLE,TABLE,public.orders, DROP TABLE orders;,<not logged>
⚠ Warning: log_statement = 'all' and pgaudit.log = 'all' cause disk/I/O explosions in production and may write passwords to logs. Ship logs to a central SIEM.
06

Encryption

LayerMethod
Data in transitssl=on + sslmode=verify-full
Column levelpgcrypto (pgp_sym_encrypt)
Data at rest (TDE)pg_tde (PG17 · Percona)
Disk levelLUKS / dm-crypt / cloud KMS
Backupspg_dump | gpg · pgBackRest cipher
pgcrypto — Column Encryption
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Symmetric encryption (bytea column) INSERT INTO customers (name, tckn_enc) VALUES ('Ayşe', pgp_sym_encrypt('12345678901', :'key')); SELECT pgp_sym_decrypt(tckn_enc, :'key') FROM customers; -- Application password: bcrypt hash SELECT crypt('s3cret', gen_salt('bf', 12));
pg_tde — Transparent Data Encryption
shared_preload_libraries = 'pg_tde' CREATE EXTENSION pg_tde; -- define key provider + principal key, then: CREATE TABLE cards (...) USING tde_heap;
Encrypted Backup
pg_dump -Fc app_db | gpg --encrypt \ --recipient [email protected] > app_db.dump.gpg gpg --decrypt app_db.dump.gpg | pg_restore -d app_db # pgBackRest: repo1-cipher-type=aes-256-cbc
Certificate & Client Verification
ALTER SYSTEM SET ssl_cert_file = 'server.crt'; ALTER SYSTEM SET ssl_key_file = 'server.key'; SELECT pg_reload_conf(); # Client: full verification against MITM psql "host=db.corp.com dbname=app_db user=app_user sslmode=verify-full sslrootcert=root.crt"
⚠ Warning: Don't embed the pgcrypto key in SQL — it can end up in logs via log_statement. sslmode=require does not verify the certificate; use verify-full. Community PostgreSQL has no built-in TDE.
07

Extension Security

ExtensionPurposeRisk
pgauditAudit trailSAFE
pgcryptoCryptographic functionsSAFE
pg_stat_statementsQuery statisticsSAFE
postgres_fdw / dblinkRemote connections — credential leakageCAUTION
adminpack / file_fdwServer file system accessHIGH
plpython3u / plperluUntrusted language — runs OS commandsHIGH
shared_preload_libraries Whitelist
# Approved libraries only — restart required shared_preload_libraries = 'pg_stat_statements,pgaudit,passwordcheck' # Current value: SHOW shared_preload_libraries;
Installation & Function Access
-- Privilege control: install extension into a dedicated schema CREATE SCHEMA ext AUTHORIZATION postgres; CREATE EXTENSION IF NOT EXISTS pgcrypto SCHEMA ext; -- Functions are granted to PUBLIC by default! REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA ext FROM PUBLIC; GRANT EXECUTE ON FUNCTION ext.pgp_sym_decrypt(bytea, text) TO app_secure; ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; -- Installed extension inventory SELECT e.extname, e.extversion, n.nspname AS schema, r.rolname AS owner FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace JOIN pg_roles r ON r.oid = e.extowner;
SECURITY DEFINER Functions
CREATE FUNCTION app.rotate_key() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- mandatory! AS $$ ... $$;
Trusted extensions (PG13+): Extensions marked trusted = true can be installed by non-superusers with CREATE privilege on the database. For tighter control, enforce a whitelist with pgextwlist.
⚠ Warning: Untrusted languages (*u) and COPY ... PROGRAM let a superuser run OS-level commands — which is why superuser = the OS user postgres.
08

Security Checklist

Go-Live Readiness
Check ItemPriority
pg_hba.conf — trust removedpg_hba_file_rules WHERE auth_method='trust'Critical
scram-sha-256 active, no md5 hashes leftSHOW password_encryption;Critical
SSL/TLS enabled, TLSv1.2+ enforcedSHOW ssl; · pg_stat_sslCritical
Superuser count ≤ 2SELECT count(*) FROM pg_roles WHERE rolsuper;Critical
public schema restricted\dn+ publicImportant
pgaudit installed and activeSHOW pgaudit.log;Important
Password policy enforced (passwordcheck + VALID UNTIL)SHOW shared_preload_libraries;Important
Backup encryption activegpg / repo1-cipher-typeImportant
RLS enabled on critical tablespg_class WHERE relrowsecurityRecommended
log_connections onSHOW log_connections;Recommended
Criticalfix now Importantthis sprint Recommendedplan
One-Query Security Audit
-- dbaguide.com · security quick audit SELECT name, setting, CASE WHEN name = 'ssl' AND setting = 'on' THEN '✓' WHEN name = 'password_encryption' AND setting = 'scram-sha-256' THEN '✓' WHEN name IN ('log_connections', 'log_disconnections') AND setting = 'on' THEN '✓' WHEN name = 'shared_preload_libraries' AND setting LIKE '%pgaudit%' THEN '✓' WHEN name = 'listen_addresses' AND setting <> '*' THEN '✓' ELSE '✗ CHECK' END AS status FROM pg_settings WHERE name IN ('ssl', 'password_encryption', 'log_connections', 'log_disconnections', 'shared_preload_libraries', 'listen_addresses'); -- Critical tables without RLS SELECT relname FROM pg_class WHERE relkind = 'r' AND NOT relrowsecurity AND relnamespace = 'app'::regnamespace;
✓ Best Practice: Run these checks weekly via CI/CD or cron and ship results to your monitoring system. Re-run the full list after every major upgrade.