Database audit fix

How to Enable & Configure SSL in PostgreSQL Server

PostgreSQL ships with ssl off, which leaves passwords and query results readable to anything on the network path once the server listens beyond localhost. Set ssl = on in postgresql.conf, give it a certificate and key, and restart.

Hiren KalariyaLast reviewed: Sep 28, 2026Check postgresql-tls
Medium
severity
Yes
needs root or sudo
4
results it can return
3 of 5
checks in this audit

Every threshold on this page is transcribed from the database-security-transport-encryption audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 5 Transport Encryption checks, or how the audit runs.

Medium severityNeeds sudo

What this check reads

Asks the running cluster for two live settings as the postgres OS user: SHOW ssl and SHOW listen_addresses. ssl reading exactly on is a PASS on its own, and the message that comes back reminds you to pair it with hostssl rules in pg_hba.conf. When ssl is off, listen_addresses decides the severity: a value of localhost, 127.0.0.1 or nothing at all is a WARN, and any wider value is a FAIL.

When it applies

Runs only when PostgreSQL is detected (HAS_PG=1) and reachable via PGOK=1; otherwise SKIP. It reads two live settings, ssl and listen_addresses, and the severity of a negative result depends on the second: TLS off while listening only on localhost is a WARN, TLS off while listening more widely is a FAIL. Note what PASS does not mean - ssl=on only makes TLS available. Whether clients are actually forced to use it is decided by hostssl versus host lines in pg_hba.conf, which this check does not read.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
PASS`ssl = on`TLS is available on the wire, though nothing here proves clients are actually made to use it.
WARN`ssl = off`, loopback onlyTraffic is unencrypted but never leaves the host, so this becomes a real exposure the moment listen_addresses widens.
FAIL`ssl = off`, network-exposedPasswords and query results are crossing a real network in clear text right now.
SKIPNot accessibleNothing was read from the cluster, so its transport setting is unverified rather than known good.

Why it matters

PostgreSQL's ssl=on makes TLS available; enforcement is done per-line in pg_hba.conf with hostssl (and hostnossl ... reject). SCRAM protects the password but not the data. Debian/Ubuntu packages enable ssl=on with a snakeoil cert by default; RHEL packages do not. CIS PostgreSQL "Ensure TLS is enabled and configured correctly".

Why it fails, and when it is wrong

  • ssl=on with only host (not hostssl) lines is a PASS here but still allows plaintext; the message tells you.
  • Snakeoil/self-signed certs give encryption without authentication; clients must use sslmode=verify-full with the CA to stop MITM.
  • Unix-socket connections are never TLS and are unaffected.
  • ssl_min_protocol_version defaults to TLSv1.2 since PostgreSQL 12; not checked.

How to fix it

Read the current state

Read the two live settings before you change anything: ssl tells you whether TLS is available at all, and listen_addresses decides how much that matters. TLS off on a cluster listening only on localhost is a warning; TLS off on one reachable across a network is a failure, because passwords and query results are crossing it in clear text right now. Both commands are in Verify the fix below.

Enable TLS and point at a certificate

Turn ssl on and give the server a certificate and key. The key must be mode 0600 and owned by the postgres user, or the server refuses to enable TLS at startup.

# postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/db.crt'
ssl_key_file  = '/etc/ssl/private/db.key'      # 0600 owned by postgres
ssl_min_protocol_version = 'TLSv1.2'

Make it mandatory per rule

ssl = on only makes TLS possible. A plain host line still accepts a client that does not ask for TLS, so enforcement lives per rule in pg_hba.conf: hostssl refuses a plaintext connection, and a hostnossl ... reject catch-all closes the rest.

# pg_hba.conf
hostssl   all all 10.0.0.0/24  scram-sha-256
hostnossl all all 0.0.0.0/0    reject

Restart and verify

SELECT pg_reload_conf(); (ssl can be changed with a reload since PostgreSQL 10). If the setting will not take effect, restart the cluster and read journalctl -u postgresql, which names the exact problem when a missing certificate file or a wrong key permission is blocking it. Then confirm a live session is actually encrypted, and that a plaintext one is refused, with the commands in Verify the fix.

Verify the fix

sudo -u postgres psql -X -tAc 'SHOW ssl'                  # expected: on
sudo -u postgres psql -X -tAc 'SHOW listen_addresses'

# Enforcement lives in pg_hba.conf - confirm no plain "host" line accepts remote traffic:
sudo -u postgres psql -X -c \
  "SELECT line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE type='host'"
# expected: no remote 'host' rows (use hostssl), or a reject catch-all

# Prove a plaintext connection is refused and an encrypted one works:
psql "host=<server> user=app dbname=appdb sslmode=disable" -c 'SELECT 1'   # expected: refused
psql "host=<server> user=app dbname=appdb sslmode=require" -c 'SELECT ssl_is_used()'

Debugging

HAS_PG=0 or PGOK=0; see the PostgreSQL Auth Methods debugging notes.

ssl requires a restart, and the server refuses to enable it if ssl_cert_file/ssl_key_file are missing or the key permissions are wrong (the key must be 0600 and owned by the postgres user). The startup error in journalctl -u postgresql names the exact problem.

The check verifies availability, not enforcement. Any host (rather than hostssl) line in pg_hba.conf lets a client negotiate plaintext. Use the pg_hba_file_rules query above and switch remote lines to hostssl.

listen_addresses is wider than you think. Check the live value, and see the PostgreSQL Network Binding check.

Deliberate and mild: local traffic does not cross a network, but any process on the host can still read the loopback interface. Turn TLS on before you ever widen listen_addresses.

Sources

How the script reads this

Next

Re-run the Transport Encryption audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 5 Transport Encryption fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals