Configure PostgreSQL 17 SSL encryption and certificate-based authentication

Advanced 45 min Aug 09, 2026 56 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up a private CA, issue server and client certificates, enforce TLS 1.2+ with strong ciphers, and configure pg_hba.conf for mutual TLS client certificate authentication in PostgreSQL 17.

Prerequisites

  • PostgreSQL 17 installed
  • Root or sudo access
  • OpenSSL installed
  • Basic familiarity with pg_hba.conf syntax

What this solves

By default, PostgreSQL connections are unencrypted unless explicitly configured otherwise, and password authentication over the network exposes credentials to interception. This tutorial covers generating a private certificate authority, issuing server and client certificates, enforcing TLS-only connections, and configuring mutual TLS so clients authenticate with certificates instead of passwords.

This setup is appropriate for production databases handling sensitive data, multi-tenant environments, and compliance-driven infrastructure where password-based authentication is insufficient.

Warning: Test all changes on a staging instance first. Misconfigured pg_hba.conf or SSL settings can lock out all connections, including local ones.

Step-by-step configuration

Install PostgreSQL 17 and OpenSSL

Install PostgreSQL 17 and the OpenSSL tools needed to build a certificate authority.

sudo apt update
sudo apt install -y postgresql-17 postgresql-client-17 openssl
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf install -y postgresql17-server postgresql17 openssl
sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
sudo systemctl enable --now postgresql-17

Create a private certificate authority

The CA signs both the server certificate and every client certificate. Keep the CA private key offline or heavily restricted after issuing certificates.

sudo mkdir -p /etc/postgresql/ssl/ca
cd /etc/postgresql/ssl/ca
sudo openssl genrsa -aes256 -out ca.key 4096
sudo openssl req -new -x509 -days 3650 -key ca.key -sha256 \
  -out ca.crt \
  -subj "/C=NL/O=Example Corp/CN=Example Corp PostgreSQL CA"

You will be prompted for a passphrase to encrypt the CA private key. Use a strong passphrase and store it in a secrets manager, not on the filesystem.

Generate the server certificate

Create a key and certificate signing request (CSR) for the PostgreSQL server. The common name must match the hostname clients use to connect.

sudo mkdir -p /etc/postgresql/ssl/server
cd /etc/postgresql/ssl/server
sudo openssl genrsa -out server.key 4096
sudo openssl req -new -key server.key -out server.csr \
  -subj "/C=NL/O=Example Corp/CN=db01.example.com"

Sign the CSR with the CA to produce the server certificate, valid for one year.

sudo openssl x509 -req -in server.csr -CA /etc/postgresql/ssl/ca/ca.crt \
  -CAkey /etc/postgresql/ssl/ca/ca.key -CAcreateserial \
  -out server.crt -days 365 -sha256

Set correct ownership and permissions on certificates

PostgreSQL refuses to start if the server private key is readable by other users. The postgres process owns and reads these files directly, so ownership must belong to the postgres user with restrictive permissions.

sudo chown postgres:postgres /etc/postgresql/ssl/server/server.key /etc/postgresql/ssl/server/server.crt
sudo chmod 600 /etc/postgresql/ssl/server/server.key
sudo chmod 644 /etc/postgresql/ssl/server/server.crt
sudo chown postgres:postgres /etc/postgresql/ssl/ca/ca.crt
sudo chmod 644 /etc/postgresql/ssl/ca/ca.crt
Never use chmod 777. The private key must only be readable by the postgres user. Granting broad access here would let any local user impersonate the server or read encrypted traffic keys.

Enable SSL in postgresql.conf

Point PostgreSQL at the certificate files and turn on SSL support.

ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server/server.crt'
ssl_key_file = '/etc/postgresql/ssl/server/server.key'
ssl_ca_file = '/etc/postgresql/ssl/ca/ca.crt'
ssl_prefer_server_ciphers = on
Note: On AlmaLinux and Rocky, the config file is at /var/lib/pgsql/17/data/postgresql.conf unless you changed the data directory during initdb.

Enforce strong TLS protocol versions and cipher suites

Restrict connections to TLS 1.2 and above, and set an explicit cipher list to block weak algorithms.

ssl_min_protocol_version = 'TLSv1.2'
ssl_max_protocol_version = 'TLSv1.3'
ssl_ciphers = 'HIGH:!aNULL:!MD5:!3DES:!RC4'
ssl_ecdh_curve = 'prime256v1'

Restart PostgreSQL to apply the SSL settings.

sudo systemctl restart postgresql

Issue a client certificate for mutual TLS

Each client certificate's common name must exactly match the PostgreSQL role it authenticates as. Generate one per application or admin user.

sudo mkdir -p /etc/postgresql/ssl/clients
cd /etc/postgresql/ssl/clients
sudo openssl genrsa -out app_user.key 4096
sudo openssl req -new -key app_user.key -out app_user.csr \
  -subj "/C=NL/O=Example Corp/CN=app_user"
sudo openssl x509 -req -in app_user.csr -CA /etc/postgresql/ssl/ca/ca.crt \
  -CAkey /etc/postgresql/ssl/ca/ca.key -CAcreateserial \
  -out app_user.crt -days 365 -sha256

Set restrictive ownership before distributing the client key to the application host.

sudo chmod 600 app_user.key
sudo chmod 644 app_user.crt

Create the matching PostgreSQL role

The role name must exactly match the certificate common name for cert authentication to succeed.

sudo -u postgres psql -c "CREATE ROLE app_user LOGIN;"
sudo -u postgres psql -c "GRANT CONNECT ON DATABASE appdb TO app_user;"

Configure pg_hba.conf for certificate authentication

Set the authentication method to cert and require clientcert verification so PostgreSQL rejects any connection lacking a CA-signed client certificate.

# TYPE  DATABASE  USER      ADDRESS         METHOD
hostssl appdb     app_user  203.0.113.0/24  cert clientcert=verify-full
hostssl all       all       0.0.0.0/0       reject

The clientcert=verify-full option checks both that the certificate is signed by the trusted CA and that its common name matches the connecting role. Reload PostgreSQL to apply the rules.

sudo systemctl reload postgresql
Note: Order matters in pg_hba.conf. Rules are evaluated top to bottom, and the first match wins.

Verify your setup

Copy the CA certificate and client certificate and key to the client machine, then connect with sslmode=verify-full to validate both the server identity and the client certificate.

psql "host=db01.example.com dbname=appdb user=app_user \
  sslmode=verify-full \
  sslrootcert=/etc/postgresql/ssl/ca/ca.crt \
  sslcert=/etc/postgresql/ssl/clients/app_user.crt \
  sslkey=/etc/postgresql/ssl/clients/app_user.key"

Confirm the connection is using SSL and check the negotiated protocol and cipher.

psql -c "SELECT ssl, version, cipher FROM pg_stat_ssl JOIN pg_stat_activity USING (pid) WHERE pid = pg_backend_pid();"

Confirm password authentication is actually rejected for the same user without a certificate.

psql "host=db01.example.com dbname=appdb user=app_user sslmode=require"

This should fail with an authentication error since no client certificate was supplied.

Rotating certificates without downtime

Certificates expire, and rotating the server certificate should not require a restart. PostgreSQL reloads SSL context on pg_reload_conf() as long as the file paths in postgresql.conf stay the same.

sudo openssl req -new -key /etc/postgresql/ssl/server/server.key \
  -out /etc/postgresql/ssl/server/server_renew.csr \
  -subj "/C=NL/O=Example Corp/CN=db01.example.com"
sudo openssl x509 -req -in /etc/postgresql/ssl/server/server_renew.csr \
  -CA /etc/postgresql/ssl/ca/ca.crt -CAkey /etc/postgresql/ssl/ca/ca.key \
  -CAcreateserial -out /etc/postgresql/ssl/server/server.crt.new -days 365 -sha256
sudo mv /etc/postgresql/ssl/server/server.crt.new /etc/postgresql/ssl/server/server.crt
sudo chown postgres:postgres /etc/postgresql/ssl/server/server.crt
sudo -u postgres psql -c "SELECT pg_reload_conf();"

For client certificates, issue the replacement before the old one expires, distribute it to the application, and update the application config to point at the new file with a rolling deploy. Since both certificates are trusted by the same CA during the transition window, there is no need to disable authentication.

Set a calendar reminder or monitoring check for certificate expiry dates well before renewal is due. If you already run PostgreSQL streaming replication with PgBouncer, apply the same server certificate rotation on standby nodes to keep replication connections consistent.

Combining with connection pooling

If you terminate connections through PgBouncer, configure PgBouncer to also require TLS between the application and the pooler, and between the pooler and PostgreSQL. See Configure PostgreSQL 17 connection pooling with PgBouncer for high availability for the pooler-side setup, then apply the sslmode=verify-full settings from this tutorial to the pooler's backend connection string.

For broader hardening beyond SSL, including password policies, connection limits, and role separation, see Configure PostgreSQL 17 SSL encryption and advanced security hardening.

Common issues

SymptomCauseFix
FATAL: no pg_hba.conf entry for host, SSL offClient connected without sslmode=require or higherAdd sslmode=verify-full to the connection string, confirm hostssl is used in pg_hba.conf
could not load private key file, permission deniedserver.key is not readable by the postgres userRun sudo chown postgres:postgres server.key && sudo chmod 600 server.key
certificate verify failed: unable to get local issuer certificateClient does not have the CA certificate, or sslrootcert path is wrongCopy ca.crt to the client and reference it with sslrootcert
FATAL: certificate authentication failed for userCertificate common name does not match the PostgreSQL role nameRegenerate the client certificate with CN matching the exact role name
SSL error: sslv3 alert handshake failureClient and server share no compatible cipher or protocol versionCheck ssl_min_protocol_version and ssl_ciphers, update client OpenSSL if outdated
connection works with sslmode=require but fails with verify-fullServer certificate CN does not match the hostname used to connectReissue server certificate with CN matching the DNS name clients use
server won't start after enabling sslMissing or misnamed certificate files in postgresql.conf pathsVerify file paths with sudo -u postgres psql -c "SHOW ssl_cert_file;" and check the PostgreSQL log

Next steps

Running this in production?

Want this handled for you? Running this at scale adds a second layer of work: certificate rotation across fleets, monitoring expiry dates, and auditing cipher policy drift after every PostgreSQL minor upgrade. See how we run infrastructure like this for European teams.

Prefere não gerir isto sozinho?

Gerimos a infraestrutura de empresas que dependem do tempo de atividade. Totalmente gerida, com um contacto fixo que conhece o seu ambiente.

Tem um contacto fixo que conhece o seu ambiente

Roterdão 05:07 · acessível por mensagem, sem formulário de tickets