PostgreSQL has supported TLS since version 7.4, yet most self-managed PostgreSQL instances I encounter in the wild are running without it. The default pg_hba.conf allows password authentication without requiring encryption, meaning credentials and query results travel in the clear across the network. This article covers enabling server-side TLS, verifying the configuration, enforcing encryption through pg_hba.conf, and encrypting replication connections between primary and standby.
How PostgreSQL TLS works
PostgreSQL uses its own TLS implementation rather than delegating to a reverse proxy. When a client connects, it sends a SSLRequest packet first. If the server has TLS enabled and the client requests TLS, a TLS handshake occurs before any credentials are exchanged. This means:
- The password (even if hashed in transit via SCRAM) is protected.
- Query results and data are encrypted.
- You can require TLS in
pg_hba.confusing thehostsslconnection type.
Prerequisites
- PostgreSQL 13 or later (examples use Debian/Ubuntu paths)
- A valid certificate in PEM format for the PostgreSQL server’s hostname
- Access to
postgresql.confandpg_hba.conf
Step 1: Prepare the certificate and key files
PostgreSQL reads its certificate from the data directory by default. Common paths:
| Distribution | Data directory |
|---|---|
| Debian/Ubuntu (apt) | /var/lib/postgresql/14/main/ |
| RHEL/Rocky (dnf) | /var/lib/pgsql/14/data/ |
| Generic source build | wherever you ran initdb |
Place the files directly in the data directory or use absolute paths in postgresql.conf.
# Copy Let's Encrypt certificate and key
sudo cp /etc/letsencrypt/live/db.example.com/fullchain.pem \
/var/lib/postgresql/14/main/server.crt
sudo cp /etc/letsencrypt/live/db.example.com/privkey.pem \
/var/lib/postgresql/14/main/server.key
# PostgreSQL requires the key to be owned by the postgres user
# and mode 0600 (no group or world access)
sudo chown postgres:postgres \
/var/lib/postgresql/14/main/server.crt \
/var/lib/postgresql/14/main/server.key
sudo chmod 600 /var/lib/postgresql/14/main/server.key
sudo chmod 644 /var/lib/postgresql/14/main/server.crt
Important: PostgreSQL is strict about the key file permissions. If
server.keyis group- or world-readable, PostgreSQL will refuse to start with:FATAL: private key file "server.key" has group or world access
Step 2: Enable TLS in postgresql.conf
Open postgresql.conf:
sudo nano /etc/postgresql/14/main/postgresql.conf
Set these parameters:
# Enable TLS
ssl = on
# Certificate and key paths (relative to data directory, or absolute)
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
# If using an internal CA, also specify the CA bundle:
# ssl_ca_file = 'root.crt'
# Minimum TLS version
ssl_min_protocol_version = 'TLSv1.2'
# Optional: restrict to strong ciphers
# ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
Step 3: Configure pg_hba.conf to require TLS
pg_hba.conf controls which connections are allowed and whether TLS is required. There are three relevant connection types:
host— allow both TLS and plain connectionshostssl— allow TLS connections onlyhostnossl— allow plain connections only (use to explicitly block non-TLS)
Open pg_hba.conf:
sudo nano /etc/postgresql/14/main/pg_hba.conf
Replace plain host lines for remote clients with hostssl:
# TYPE DATABASE USER ADDRESS METHOD
# Local connections (socket, no TLS needed)
local all all peer
# Require TLS for all remote connections
hostssl all all 0.0.0.0/0 scram-sha-256
hostssl all all ::/0 scram-sha-256
# Block non-TLS remote connections
hostnossl all all 0.0.0.0/0 reject
This configuration:
- Allows local (unix socket) connections without TLS — these never leave the host.
- Requires TLS + SCRAM authentication for any TCP connection.
- Explicitly rejects plain TCP connections.
Step 4: Reload PostgreSQL
A configuration reload (not a full restart) is sufficient for most changes:
sudo systemctl reload postgresql
# or on older systems:
sudo -u postgres psql -c "SELECT pg_reload_conf();"
If you changed ssl = off to ssl = on, a full restart is required:
sudo systemctl restart postgresql
Check the logs:
sudo tail -50 /var/log/postgresql/postgresql-14-main.log
Look for:
LOG: database system is ready to accept connections
Without any TLS-related errors.
Step 5: Verify TLS from a client
Using psql:
psql "host=db.example.com user=myuser dbname=mydb sslmode=require"
Inside psql, check the connection:
SELECT ssl, version, cipher, bits FROM pg_stat_ssl WHERE pid = pg_backend_pid();
Expected output:
ssl | version | cipher | bits
-----+---------+---------------------------------------+------
t | TLSv1.3 | TLS_AES_256_GCM_SHA384 | 256
Using openssl:
openssl s_client -connect db.example.com:5432 -starttls postgres -servername db.example.com </dev/null 2>&1 \
| openssl x509 -noout -dates -subject
Note: PostgreSQL uses STARTTLS (upgrade from plain to TLS within the connection), not direct TLS, so you need -starttls postgres with openssl.
Step 6: Configure the client to verify the server certificate
The sslmode setting on the client side controls how strictly the certificate is verified:
| sslmode | Encryption | Certificate verified |
|---|---|---|
disable | No | No |
allow | If available | No |
prefer | If available | No |
require | Yes | No (only encryption) |
verify-ca | Yes | CA checked |
verify-full | Yes | CA + hostname checked |
For production workloads, use verify-full:
PGSSLMODE=verify-full \
PGSSLROOTCERT=/etc/ssl/certs/my-ca.crt \
psql "host=db.example.com user=myuser dbname=mydb"
In connection strings:
postgresql://myuser@db.example.com/mydb?sslmode=verify-full&sslrootcert=/etc/ssl/certs/my-ca.crt
Application frameworks (SQLAlchemy, JDBC, Django) accept the same sslmode and sslrootcert parameters in their connection URLs.
Step 7: Encrypting replication connections (streaming replication)
If you run PostgreSQL with streaming replication, the replication connection also needs TLS. The standby connects to the primary using a replication user.
On the primary, pg_hba.conf must allow TLS for replication:
hostssl replication replicator standby-ip/32 scram-sha-256
In the standby’s recovery.conf (PostgreSQL 11 and earlier) or postgresql.conf (PostgreSQL 12+):
primary_conninfo = 'host=primary.example.com port=5432 user=replicator ****** sslmode=verify-full sslrootcert=/var/lib/postgresql/14/main/root.crt'
Copy the primary’s CA certificate (or self-signed server certificate) to the standby:
scp primary:/var/lib/postgresql/14/main/server.crt \
standby:/var/lib/postgresql/14/main/root.crt
chown postgres:postgres /var/lib/postgresql/14/main/root.crt
Restart the standby to apply the new primary_conninfo.
Certificate renewal automation
For Let’s Encrypt certificates, the renewal hook must copy files to the PostgreSQL data directory and fix permissions:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/postgresql.sh
#!/bin/bash
DOMAIN=db.example.com
DATA_DIR=/var/lib/postgresql/14/main
cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem ${DATA_DIR}/server.crt
cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem ${DATA_DIR}/server.key
chown postgres:postgres ${DATA_DIR}/server.crt ${DATA_DIR}/server.key
chmod 644 ${DATA_DIR}/server.crt
chmod 600 ${DATA_DIR}/server.key
# A reload is sufficient to pick up new certificates
systemctl reload postgresql
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/postgresql.sh
Mutual TLS — client certificate authentication
If you want PostgreSQL to authenticate clients by certificate rather than (or in addition to) password, use cert as the authentication method in pg_hba.conf:
hostssl mydb appuser 10.0.0.0/8 cert clientcert=verify-full
The client must then present a certificate signed by the CA specified in ssl_ca_file. The CN of the client certificate is used as the PostgreSQL username.
psql "host=db.example.com user=appuser dbname=mydb \
sslcert=/etc/app/client.crt sslkey=/etc/app/client.key \
sslrootcert=/etc/app/server-ca.crt sslmode=verify-full"
Common errors
| Error | Cause | Fix |
|---|---|---|
FATAL: private key file has group or world access | server.key mode is too permissive | chmod 600 server.key |
no pg_hba.conf entry for host ... SSL off | Client connects without TLS but only hostssl rule exists | Use sslmode=require or higher in the client |
SSL error: certificate verify failed | Client does not trust the server CA | Provide sslrootcert pointing to the CA |
SSL off in pg_stat_ssl | TLS is disabled or client disabled it | Check ssl = on in postgresql.conf; use sslmode=require |
server does not support SSL | ssl=off in postgresql.conf | Enable ssl=on and restart |
Summary
Enabling TLS on PostgreSQL requires setting ssl = on in postgresql.conf, placing a certificate and key in the data directory with the correct permissions, and updating pg_hba.conf to use hostssl for remote connections. Once enabled, clients should use sslmode=verify-full with the server’s CA certificate to protect against man-in-the-middle attacks. Replication traffic is secured by adding TLS parameters to primary_conninfo on the standby. Certificate renewal is automated with a deploy hook that copies new files and triggers a PostgreSQL reload.