How to Secure PostgreSQL for Production
A default PostgreSQL installation is not production-ready from a security perspective. The superuser is trusted locally without a password, network access may be open, and logging is minimal. This guide takes a fresh PostgreSQL 16 install and makes it fit for production.
Step 1: Lock Down Network Access
# postgresql.conf — restrict which interfaces PostgreSQL listens on
# Default is localhost only, but verify:
grep listen_addresses /etc/postgresql/16/main/postgresql.conf
# listen_addresses = 'localhost' (good — only listen locally)
# If your app is on the same host, localhost is correct.
# If you need remote connections, list specific IPs:
# listen_addresses = '192.168.1.50, 10.0.0.50'
# Never: listen_addresses = '*' (exposes to all interfaces)
# pg_hba.conf — control who can connect
# File location:
sudo -u postgres psql -c "SHOW hba_file;"
# Secure pg_hba.conf (example)
sudo nano /etc/postgresql/16/main/pg_hba.conf
# pg_hba.conf — secure configuration
# TYPE DATABASE USER ADDRESS METHOD
# Superuser: local socket only, not from network
local all postgres peer
# Application user: only from specific IP, require password, require SSL
hostssl appdb appuser 10.0.0.0/24 scram-sha-256
# Deny all other connections
host all all 0.0.0.0/0 reject
host all all ::/0 reject
# Reload after editing
sudo systemctl reload postgresql
Step 2: Change the Default Superuser Password
sudo -u postgres psql
-- Set a strong password for the postgres superuser
\password postgres
-- Or via psql one-liner:
sudo -u postgres psql -c "\password postgres"
Better: disable remote login for the postgres user entirely (the peer auth in pg_hba.conf above already does this for local socket connections from any user other than the postgres OS user).
Step 3: Enforce SSL/TLS
# Enable SSL in postgresql.conf
sudo nano /etc/postgresql/16/main/postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/postgresql.crt'
ssl_key_file = '/etc/ssl/private/postgresql.key'
ssl_ca_file = '/etc/ssl/certs/ca.crt' # for client cert verification
ssl_min_protocol_version = 'TLSv1.2'
ssl_ciphers = 'HIGH:!aNULL:!MD5'
# Generate a self-signed certificate (or use your CA)
openssl req -new -x509 -days 3650 -nodes \
-out /etc/ssl/certs/postgresql.crt \
-keyout /etc/ssl/private/postgresql.key \
-subj "/CN=postgres.internal"
sudo chown postgres:postgres /etc/ssl/private/postgresql.key
sudo chmod 600 /etc/ssl/private/postgresql.key
# Force SSL for the application connection
# In pg_hba.conf: use "hostssl" instead of "host"
# This rejects plaintext connections from the application
sudo systemctl restart postgresql
# Verify SSL is working
psql "host=localhost dbname=appdb user=appuser sslmode=require" -c "\conninfo"
# SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)
Step 4: Create Least-Privilege Roles
-- Connect as superuser
sudo -u postgres psql
-- Create a dedicated application role with no superuser privileges
CREATE ROLE appuser WITH LOGIN PASSWORD 'strong-random-password-here';
-- Create the database owned by a separate owner role
CREATE ROLE appowner NOLOGIN;
CREATE DATABASE appdb OWNER appowner;
-- Connect to the application database
\c appdb
-- Grant only what the application needs
-- Typical CRUD application:
GRANT CONNECT ON DATABASE appdb TO appuser;
GRANT USAGE ON SCHEMA public TO appuser;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO appuser;
-- Future tables and sequences inherit permissions
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO appuser;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO appuser;
-- Read-only replica user
CREATE ROLE readuser WITH LOGIN PASSWORD 'another-strong-password';
GRANT CONNECT ON DATABASE appdb TO readuser;
GRANT USAGE ON SCHEMA public TO readuser;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readuser;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO readuser;
-- Revoke public schema creation from PUBLIC (default in Postgres 14 and older)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- Verify privileges
\dp -- show table privileges
\du -- show users and roles
Step 5: Enable Audit Logging
# postgresql.conf — logging configuration
log_destination = 'syslog'
syslog_facility = 'LOCAL0'
syslog_ident = 'postgres'
# What to log
log_connections = on
log_disconnections = on
log_failed_attempts = on
log_hostname = off # avoid DNS lookups in logs
# Log slow queries (useful for both performance and detecting attacks)
log_min_duration_statement = 1000 # log queries slower than 1 second
# Log all DDL (CREATE, DROP, ALTER)
log_statement = 'ddl'
# Log all connections from IP ranges you do not expect
# (use pg_hba.conf to deny them, but log helps detect attempts)
For more detailed auditing, install pgaudit:
# Install pgaudit
sudo apt install postgresql-16-pgaudit
# postgresql.conf
shared_preload_libraries = 'pgaudit'
# pgaudit settings
pgaudit.log = 'ddl, write, role'
pgaudit.log_catalog = off
pgaudit.log_parameter = on
pgaudit.log_statement_once = off
-- Enable pgaudit per role (audit all writes by the app user)
ALTER ROLE appuser SET pgaudit.log = 'write';
Step 6: Restrict Row-Level Access
Row-Level Security (RLS) ensures users can only see their own data even if they access the table:
-- Enable RLS on a multi-tenant table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Create a policy: each user sees only their own orders
CREATE POLICY user_orders ON orders
USING (user_id = current_setting('app.current_user_id')::int);
-- Application sets this session variable per request
-- In your app code:
-- SET LOCAL app.current_user_id = '42';
-- SELECT * FROM orders; -- returns only orders for user 42
Step 7: Encrypted Backups
#!/bin/bash
# pg-backup-encrypted.sh
DB_NAME="appdb"
BACKUP_DIR="/var/backups/postgresql"
DATE=$(date +%Y-%m-%d-%H%M)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}-${DATE}.sql.gz.age"
RECIPIENTS_FILE="/root/.age-recipients" # age public keys
mkdir -p "$BACKUP_DIR"
# Dump and encrypt in a pipeline — plaintext never touches disk
pg_dump -U postgres "$DB_NAME" \
| gzip \
| age -R "$RECIPIENTS_FILE" \
> "$BACKUP_FILE"
echo "Backup written: $BACKUP_FILE ($(du -sh "$BACKUP_FILE" | cut -f1))"
# Keep 30 days of backups
find "$BACKUP_DIR" -name "*.age" -mtime +30 -delete
# Restore
age -d -i ~/.age/identity.txt backup.sql.gz.age | gunzip | psql -U postgres appdb
Step 8: Rotate Credentials
# Script to rotate the application user password and update the secret manager
NEW_PASSWORD=$(openssl rand -base64 32)
sudo -u postgres psql -c "ALTER ROLE appuser PASSWORD '${NEW_PASSWORD}';"
# Update in your secret manager (example: AWS)
aws secretsmanager put-secret-value \
--secret-id "prod/appdb/appuser" \
--secret-string "{\"password\":\"${NEW_PASSWORD}\"}"
echo "Password rotated. Restart application to pick up new credentials."
Related Articles
- Secure Redis Deployment Without Exposure
- How to Set Up Secure File Sharing for Sensitive Documents
- How to Secure NAS Storage for Home Use
- Secure Shell Hardening Beyond SSH Config
- Best Password Manager with Secure Notes: A Technical Guide
- AI Coding Assistant Session Data Lifecycle Built by theluckystrike — More at zovo.one