DB Internals AtlasPostgreSQL 18.6 Home About

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'   # default

All 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)

sslmodeeavesdroppingactive attacker (MITM)
disablenono
allowno (tries plain first)no
prefer (default)only if nobody interferesno: an attacker just answers "no SSL"
requireyesno: any certificate is accepted*
verify-cayespartly: any cert from a trusted CA
verify-fullyesyes

* 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_ciphers only governs TLS 1.2 and older). Empty means the OpenSSL default.
  • ssl_groups replaces ssl_ecdh_curve and accepts a colon-separated list of key-exchange groups; default X25519: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):

server does not support SSL, but SSL was requiredserver certificate for "evil.example.net" does not match host name "db.example.com"SSL error: certificate verify failed: self-signed certificate

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 encrypted
  • ProcessSSLStartup() and ProcessStartupPacket() 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 the SSL object from the shared SSL_context, installs alpn_cb() (accepts only PG_ALPN_PROTOCOL), runs the handshake, and stores port->ssl_in_use, alpn_used, and the client certificate's peer_cn/peer_dn (used later by CheckCertAuth() in auth.c). From then on be-secure.c secure_read()/secure_write() route all protocol traffic through be_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 with PG_TEST_EXTRA=ssl, and the test certificates are generated by src/test/ssl/sslfiles.mk. contrib/sslinfo exposes certificate fields to SQL.
  • Background: README.SSL has 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