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 | When | What 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 only | Traffic is unencrypted but never leaves the host, so this becomes a real exposure the moment listen_addresses widens. |
| FAIL | `ssl = off`, network-exposed | Passwords and query results are crossing a real network in clear text right now. |
| SKIP | Not accessible | Nothing 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=onwith onlyhost(nothostssl) 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-fullwith the CA to stop MITM. - Unix-socket connections are never TLS and are unaffected.
ssl_min_protocol_versiondefaults 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 rejectRestart 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
- PostgreSQL: Secure TCP/IP Connections with SSL
- PostgreSQL: SSL settings (ssl, ssl_min_protocol_version)
- PostgreSQL: pg_hba.conf hostssl/hostnossl
- PostgreSQL: libpq sslmode (verify-full)
- PostgreSQL: pg_stat_ssl view (who is encrypted right now)
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