6 min read

Postgres SSL Connections: Doing It Properly Without Losing a Day

SSL on Postgres is a one-line config change to enable and a multi-day project to do correctly. The default settings are not good enough.

Enabling SSL on Postgres takes about thirty seconds. Doing it properly — with proper certificate verification, certificate rotation, and mandatory TLS — takes longer than people expect because the defaults are weak in subtle ways.

Here is the framework I use to get it right the first time.

What SSL on Postgres actually does

With SSL enabled, the wire protocol between client and server is encrypted. Without it, anyone with network access between client and server can read every query and result.

For cloud Postgres on the open internet, SSL is non-negotiable. For internal-only Postgres, SSL is still a good idea — modern "defense in depth" assumes the network is hostile.

What SSL does not do: authenticate the user. SSL secures the channel; authentication still happens via password, certificate, IAM, etc. The two are independent.

The minimum to enable

# postgresql.conf
ssl = on
ssl_cert_file = '/etc/postgresql/16/main/server.crt'
ssl_key_file = '/etc/postgresql/16/main/server.key'

The .crt and .key are typically a self-signed certificate or one issued by your own CA. Postgres serves this on the standard port; clients can connect over SSL.

This is the 30-second version. It encrypts traffic. It does NOT verify the server's identity.

Client-side sslmode levels

The client controls the verification level via sslmode:

  • disable: no SSL at all. Refuse on production.
  • allow: prefer non-SSL, fall back to SSL if forced. Refuse on production.
  • prefer (often default): try SSL, fall back to non-SSL if rejected. Trafic could end up cleartext.
  • require: SSL or fail. Encrypts traffic but does NOT verify the server.
  • verify-ca: SSL with verified server certificate (signed by trusted CA).
  • verify-full: SSL with verified certificate AND hostname match. The strict mode.

For production clients, verify-full is the right setting. Anything less leaves you vulnerable to man-in-the-middle attacks.

Enforcing SSL server-side

The sslmode is client-controlled. To force SSL regardless of client setting, configure pg_hba.conf:

# pg_hba.conf — only allow SSL connections from external clients
hostssl  all  all  0.0.0.0/0  scram-sha-256
hostnossl all all  0.0.0.0/0  reject

The hostssl rule matches only SSL connections. The hostnossl rule rejects non-SSL connections. Together, every external connection must use SSL or fail.

For managed Postgres (RDS, Cloud SQL, Azure), the cloud provider gives you a parameter to require SSL — rds.force_ssl = 1 on RDS, etc. The mechanism is similar to the above.

The certificate part

For verify-full to work, the client needs to trust the server's certificate. Three patterns:

1. Self-signed CA, distributed to clients. You generate a CA certificate, sign the server cert with it, distribute the CA cert to every client. Clients use sslrootcert=/path/to/ca.crt.

Works but operationally awkward at scale.

2. Cloud provider's CA. RDS, Cloud SQL, etc. provide their own root CA. Download it, distribute to clients, configure sslrootcert.

The rotation cadence varies by cloud (RDS rotates every few years). When rotation happens, every client needs the new CA installed before the cutoff date or connections start failing.

3. Public CA. Get a certificate from Let's Encrypt or a commercial CA. Clients automatically trust it via the OS's CA store. No distribution needed.

Works for Postgres on public hostnames, harder for internal hostnames.

Certificate rotation

Certificates expire. You need a renewal process:

  • Cloud-provided certificates: rotation is the cloud's responsibility, with clear deadlines for clients. Mark the deadlines in calendars; rotate clients in advance.
  • Self-managed: rotate annually or biannually. Use a tool (cert-manager on K8s, Vault, etc.) to automate. Manual rotation always misses the deadline.
  • Let's Encrypt: 90-day certs, automated renewal via the LE ACME client.

Reloading Postgres picks up the new cert without restart:

SELECT pg_reload_conf();

This avoids downtime during rotation.

Common failure modes

Client expired cert error: the client did not get the rotated CA in time. Update the client's sslrootcert and reconnect.

Hostname mismatch: client connects to db.example.com but the server's cert says db-master.example.com. With verify-full, this fails. Either fix the cert to include both, or have clients connect to the right hostname.

"SSL error: tlsv1 alert protocol version": client and server cannot agree on TLS version. Newer Postgres versions default to TLS 1.2+; very old clients may need explicit TLS version configuration.

ssl_max_protocol_version blocking modern clients: a server configured to only allow TLS 1.0 (because old) but clients are TLS 1.3. Set ssl_min_protocol_version = 'TLSv1.2' and remove the max if it's set.

Performance impact

SSL adds overhead — TLS handshake on every connection plus encryption per byte. The handshake cost is mitigated by long-lived connections (the application's pool keeps connections open). The encryption cost on modern hardware with AES-NI is negligible.

For latency-sensitive workloads, the only real cost is the initial handshake on new connections. Pool wisely.

What I do for new Postgres installations

The checklist:

  1. SSL enabled with strong cipher suites (ssl_ciphers = 'HIGH:!aNULL:!MD5').
  2. Server cert signed by a CA the clients trust (cloud-provided or self-managed).
  3. pg_hba.conf requires hostssl for all non-local connections.
  4. Clients configured with sslmode=verify-full and the correct sslrootcert.
  5. Certificate rotation automated.
  6. Monitoring on certificate expiry (alert 30 days before).

This is more work than "check the SSL box." It is the difference between "we have SSL" and "we have SSL that protects what we think it protects."

Most teams I have worked with have the easy version. Upgrading to the strict version is a one-week project that pays for itself the first time anyone audits the security model.