Connections and security
SSL/TLS encryption
PostgreSQL can wrap the client connection in TLS. The client's sslmode decides whether encryption is optional or required and whether the server's certificate is checked, which is what actually stops an attacker in the middle.
In plain words
Without encryption anyone on the network path can read your queries and results, and could even change them. With TLS (still often called SSL) the connection becomes an encrypted tunnel.
Encryption alone is not enough, though: you also need to know you are talking to the real server and not an impostor in the middle. That is what the server's certificate is for, and it only helps if the client checks it. The client setting sslmode chooses: disable, allow and prefer (the default!) make encryption optional; require insists on encryption but trusts any certificate; verify-ca checks the certificate was signed by a trusted authority; verify-full also checks it was issued for the host name you connected to.
Pick an sslmode and add an attacker to see what each level really protects against.
For DBAs and operators
Server side
ssl = on
ssl_cert_file = 'server.crt' # chain, leaf first
ssl_key_file = 'server.key' # mode 0600, owned by postgres
ssl_ca_file = 'root.crt' # only to verify CLIENT certs
ssl_min_protocol_version = 'TLSv1.2' # defaultAll SSL settings take effect on reload. Force encryption for clients with hostssl lines in pg_hba.conf (and hostnossl … reject); add clientcert=verify-full or method cert to require client certificates.
sslmode (client, libpq)
| sslmode | eavesdropping | active attacker (MITM) |
|---|---|---|
disable | no | no |
allow | no (tries plain first) | no |
prefer (default) | only if nobody interferes | no: an attacker just answers "no SSL" |
require | yes | no: any certificate is accepted* |
verify-ca | yes | partly: any cert from a trusted CA |
verify-full | yes | yes |
* If a root certificate file exists (~/.postgresql/root.crt), require behaves like verify-ca. With sslrootcert=system (PostgreSQL 16+) libpq uses the OS trust store and enforces verify-full, because any public CA could otherwise vouch for an attacker.
Negotiation and direct SSL (PostgreSQL 17+)
Traditionally the client sends an 8-byte SSLRequest in clear text and the server answers one byte, S or N; only then does the TLS handshake start. Since PostgreSQL 17 a client with sslnegotiation=direct starts the TLS handshake immediately, saving a round trip; the server requires the TLS ALPN protocol postgresql for such connections, which also makes it friendly to TLS-routing proxies. Direct mode requires sslmode=require or stronger.
New in PostgreSQL 18
ssl_tls13_ciphers: the TLS 1.3 cipher suites (ssl_ciphersonly governs TLS 1.2 and older). Empty means the OpenSSL default.ssl_groupsreplacesssl_ecdh_curveand accepts a colon-separated list of key-exchange groups; defaultX25519:prime256v1.
Checking connections
SELECT a.usename, a.client_addr, s.ssl, s.version, s.cipher, s.bits
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend';In psql, \conninfo prints the protocol and cipher. Typical client-side errors (real libpq texts):
Recommended: sslmode=verify-full with your own CA in sslrootcert. require stops passive sniffing only.
For developers: inside the source
Server: negotiation
BackendInitialize() tcop/backend_startup.c
ProcessSSLStartup(port) peeks 1st byte: 0x16 = direct TLS ClientHello
secure_open_server() → be_tls_open_server() ALPN must be "postgresql"
ProcessStartupPacket(port, ssl_done, gss_done)
NEGOTIATE_SSL_CODE → send 'S' or 'N'
'S': secure_open_server() → be_tls_open_server() → SSL_accept()
then read the real startup packet, now encryptedProcessSSLStartup()andProcessStartupPacket()implement both paths; a direct connection without ALPN fails with "received direct SSL connection request without ALPN protocol negotiation extension".be_tls_open_server()creates theSSLobject from the sharedSSL_context, installsalpn_cb()(accepts onlyPG_ALPN_PROTOCOL), runs the handshake, and storesport->ssl_in_use,alpn_used, and the client certificate'speer_cn/peer_dn(used later byCheckCertAuth()inauth.c). From then onbe-secure.csecure_read()/secure_write()route all protocol traffic throughbe_tls_read()/be_tls_write().
Server: configuration and reload
secure_initialize() is called by the postmaster at start and on every SIGHUP; it calls be_tls_init(), which builds a complete new SSL_CTX: certificate chain and key (with ssl_passphrase_command via ssl_external_passwd_cb()), CA and CRL, protocol range via ssl_protocol_version_to_openssl(), SSL_CTX_set_cipher_list() for ssl_ciphers, SSL_CTX_set_ciphersuites() for ssl_tls13_ciphers, and key-exchange groups in initialize_ecdh() (ssl_groups). Only when everything succeeds does it replace SSL_context; on reload errors are logged at LOG and the old context stays in use (at start-up they are FATAL).
pg_stat_ssl is filled once per backend by pgstat_bestart_security() (backend_status.c) from be_tls_get_version(), be_tls_get_cipher(), be_tls_get_cipher_bits() into PgBackendSSLStatus.
Client: libpq
PQconnectPoll() in fe-connect.c drives sslmode/sslnegotiation (state CONNECTION_SSL_STARTUP) and the fallback between plain and SSL attempts for allow/prefer. In fe-secure-openssl.c, initialize_SSL() loads sslrootcert (for system: SSL_CTX_set_default_verify_paths()), open_client_SSL() runs the handshake, and for verify-full pgtls_verify_peer_name_matches_certificate_guts() compares the host with the certificate's SAN entries (the CN is consulted only when there is no SAN of the same type), matching names with pq_verify_peer_name_matches_certificate_name() in fe-secure-common.c, which allows a *. wildcard in the leftmost label.
Experiment
trace_connection_negotiation = on(developer option, server start) logs "SSLRequest accepted", "SSLRequest rejected", "direct SSL connection accepted" for every connection.src/test/ssl/t/001_ssltests.pl,002_scram.pl(channel binding),003_sslinfo.pl; they run only withPG_TEST_EXTRA=ssl, and the test certificates are generated bysrc/test/ssl/sslfiles.mk.contrib/sslinfoexposes certificate fields to SQL.- Background:
README.SSLhas the negotiation diagrams.
Parameters that shape this
ssl ssl_cert_file ssl_key_file ssl_ca_file ssl_crl_file ssl_min_protocol_version ssl_max_protocol_version ssl_ciphers ssl_tls13_ciphers ssl_groups ssl_prefer_server_ciphers ssl_passphrase_command
Related topics
Client authentication Wire protocol Roles and privileges Postmaster