Enabling TLS on PostgreSQL: Encrypting Client Connections and Replication Traffic

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.conf using the hostssl connection 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.conf and pg_hba.conf

Step 1: Prepare the certificate and key files

PostgreSQL reads its certificate from the data directory by default. Common paths:

DistributionData directory
Debian/Ubuntu (apt)/var/lib/postgresql/14/main/
RHEL/Rocky (dnf)/var/lib/pgsql/14/data/
Generic source buildwherever 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.key is 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 connections
  • hostssl — allow TLS connections only
  • hostnossl — 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:

sslmodeEncryptionCertificate verified
disableNoNo
allowIf availableNo
preferIf availableNo
requireYesNo (only encryption)
verify-caYesCA checked
verify-fullYesCA + 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

ErrorCauseFix
FATAL: private key file has group or world accessserver.key mode is too permissivechmod 600 server.key
no pg_hba.conf entry for host ... SSL offClient connects without TLS but only hostssl rule existsUse sslmode=require or higher in the client
SSL error: certificate verify failedClient does not trust the server CAProvide sslrootcert pointing to the CA
SSL off in pg_stat_sslTLS is disabled or client disabled itCheck ssl = on in postgresql.conf; use sslmode=require
server does not support SSLssl=off in postgresql.confEnable 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.

Scroll to Top