Build a disposable local TLS path with a private CA and hostname-valid certificate, compare libpq sslmode=require/verify-ca/verify-full, inspect pg_stat_ssl, and practice safe certificate rotation without confusing encryption with identity verification.

TLS Configuration, Certificate Verification, Channel Security, and Rotation

Build a disposable local TLS path with a private CA and hostname-valid certificate, compare libpq sslmode=require/verify-ca/verify-full, inspect pg_stat_ssl, and practice safe certificate rotation without confusing encryption with identity verification.

Intermediate → Advanced180–240 minutesPostgreSQL security engineeringCurrent patched PostgreSQL 18.x; verify current minor at lab timeCore PostgreSQL; OpenSSL used only for the free local TLS labServiceHub disposable objects: app.ch20_* and ch20_* rolesAdmin/superuser required for HBA/TLS/server-role exercisesLocal/free tooling; no paid identity provider requiredLast reviewed: August 2026

Learning outcomes

A SCRAM password can authenticate a user, but a client must also know it is talking to the intended PostgreSQL server rather than an attacker that can observe or relay network traffic. Transport Layer Security (TLS)—called SSL in PostgreSQL parameter names for historical reasons—encrypts the channel. Certificate verification adds server identity.

01

Create a disposable local certificate authority and a server certificate valid for localhost.

02

Configure a separate PostgreSQL TLS lab instance and require hostssl for the test login.

03

Distinguish sslmode=require, verify-ca, and verify-full with deterministic success/failure tests.

04

Inspect TLS protocol/cipher state using pg_stat_ssl.

05

Rotate certificate files through configuration reload and verify that old SSL configuration survives a bad reload.

Key distinction

Encryption answers 'can an observer read this connection?' Identity verification answers 'is this the server I intended to reach?' sslmode=require gives encryption but not full hostname identity verification; verify-full performs both trust-chain and host-name verification.

1. Use a separate disposable cluster

Do not experiment with private keys in the normal ServiceHub lab. Create a separate PostgreSQL data directory on port 55442. The exact binaries/paths differ by platform, but the mechanism is the same.

shell · initialize the TLS lab
initdb -D ./ch20_tls_pgdatapg_ctl -D ./ch20_tls_pgdata -o "-p 55442" -l ./ch20_tls.log start

2. Build a private lab CA and hostname-valid server certificate

This lab uses OpenSSL and a private test Certificate Authority (CA). Do not reuse its keys outside the disposable environment.

shell · create the private CA
mkdir -p ./ch20_tls_certscd ./ch20_tls_certsopenssl req -new -x509 -nodes -days 30   -newkey rsa:3072   -keyout root.key   -out root.crt   -subj "/CN=ServiceHub Chapter20 Lab Root CA"   -addext "basicConstraints=critical,CA:TRUE"   -addext "keyUsage=critical,keyCertSign,cRLSign"chmod 600 root.key
shell · create a localhost server key and CSR
openssl req -new -nodes   -newkey rsa:3072   -keyout server.key   -out server.csr   -subj "/CN=localhost"chmod 600 server.keycat > server.ext <<'EOF'subjectAltName=DNS:localhostextendedKeyUsage=serverAuthEOFopenssl x509 -req   -in server.csr   -CA root.crt   -CAkey root.key   -CAcreateserial   -days 30   -out server.crt   -extfile server.ext

The certificate deliberately has DNS:localhost but no IP-address Subject Alternative Name. That lets the lesson prove the difference between connecting to localhost and 127.0.0.1 under verify-full.

3. Install server files and enable TLS

shell · copy certificate and key into the disposable PGDATA
cp server.crt ../ch20_tls_pgdata/server.crtcp server.key ../ch20_tls_pgdata/server.keychmod 600 ../ch20_tls_pgdata/server.key

On Unix systems, PostgreSQL rejects an overly permissive private key; 0600 is the standard direct-owner mode. Windows ACL handling differs. The private key proves possession of the server identity key and must not be exposed to database clients.

conf · postgresql.conf settings
ssl = onssl_cert_file = 'server.crt'ssl_key_file = 'server.key'
conf · pg_hba.conf: require TLS for the lab login
# TYPE    DATABASE        USER             ADDRESS         METHODhostssl   servicehub_tls  ch20_tls_login   127.0.0.1/32    scram-sha-256hostssl   servicehub_tls  ch20_tls_login   ::1/128         scram-sha-256hostnossl all             all              127.0.0.1/32    rejecthostnossl all             all              ::1/128         reject

Restart the disposable server if ssl=on was not already active. HBA's hostssl restricts matching to TLS connections; a plain TCP attempt falls to the explicit hostnossl rejection.

4. Create the TLS database/login

sql · disposable TLS identity
CREATE DATABASE servicehub_tls;CREATE ROLE ch20_tls_login LOGIN;SET password_encryption = 'scram-sha-256';-- Set the password interactively in psql:-- \password ch20_tls_loginGRANT CONNECT ON DATABASE servicehub_tls TO ch20_tls_login;

5. sslmode=require encrypts but does not prove the host name

shell · encrypted connection without full identity verification
psql "host=127.0.0.1 port=55442 dbname=servicehub_tls user=ch20_tls_login sslmode=require application_name=ch20_tls_require" 

This can succeed even though the certificate says localhost, because require primarily insists on encryption. A security-sensitive client should explicitly request certificate verification rather than relying on historical/default behavior.

6. verify-ca validates the trust chain

shell · trust the private CA
psql "host=127.0.0.1 port=55442 dbname=servicehub_tls user=ch20_tls_login sslmode=verify-ca sslrootcert=./ch20_tls_certs/root.crt application_name=ch20_tls_verify_ca" 

verify-ca checks whether the presented server certificate chains to a trusted root. It does not require the connection's requested host name to match the certificate identity.

7. verify-full adds host identity

shell · correct host name succeeds
psql "host=localhost port=55442 dbname=servicehub_tls user=ch20_tls_login sslmode=verify-full sslrootcert=./ch20_tls_certs/root.crt application_name=ch20_tls_verify_full" 
shell · wrong identity fails deterministically
psql "host=127.0.0.1 port=55442 dbname=servicehub_tls user=ch20_tls_login sslmode=verify-full sslrootcert=./ch20_tls_certs/root.crt application_name=ch20_tls_bad_name" 

The IP connection should fail certificate-name verification because the certificate has DNS:localhost and no matching IP SAN. The repair is not to weaken the client to require; issue a certificate whose SANs match the real names/IPs clients use, then connect with verify-full.

8. Observe the negotiated TLS channel

sql · pg_stat_ssl joined to session identity
SELECT a.pid, a.usename, a.datname, a.application_name,       s.ssl, s.version, s.cipher, s.bits,       s.client_dn, s.client_serial, s.issuer_dnFROM pg_stat_activity AS aJOIN pg_stat_ssl AS s USING (pid)WHERE a.application_name LIKE 'ch20_tls_%'ORDER BY a.pid;

This proves that a backend is using TLS and exposes protocol/cipher information. A NULL client DN is expected because this lab uses password authentication, not mutual TLS client certificates.

9. Rotation is a file + reload + verification workflow

PostgreSQL reads TLS certificate/key/CA/CRL files at startup and on configuration reload. If replacement files are invalid during a reload, PostgreSQL logs the problem and keeps the previous SSL configuration rather than replacing it with broken credentials.

shell · rotation sequence
# 1. Issue a new certificate/key with the same required SANs.# 2. Validate key/certificate before installation.openssl x509 -in new-server.crt -noout -subject -issuer -datesopenssl pkey -in new-server.key -check -noout# 3. Atomically install with correct ownership/mode.cp new-server.crt ./ch20_tls_pgdata/server.crt.newcp new-server.key ./ch20_tls_pgdata/server.key.newchmod 600 ./ch20_tls_pgdata/server.key.newmv ./ch20_tls_pgdata/server.crt.new ./ch20_tls_pgdata/server.crtmv ./ch20_tls_pgdata/server.key.new ./ch20_tls_pgdata/server.key
sql · reload and validate new connections
SELECT pg_reload_conf();SELECT now() AS verified_at,       current_setting('ssl') AS ssl_enabled,       current_setting('ssl_cert_file') AS cert_file,       current_setting('ssl_key_file') AS key_file;

After reload, make a new verify-full connection and inspect pg_stat_ssl plus certificate dates externally. Existing TLS sessions keep the channel/certificate negotiation they already established.

10. Mutual TLS is a separate client-identity choice

If the server is configured with ssl_ca_file, a hostssl HBA rule can require a client certificate using cert authentication or clientcert=verify-ca/verify-full alongside another method. That is different from the server-certificate verification the client performs with sslmode=verify-full.

Production judgment

Use verify-full for security-sensitive libpq connections when you control certificate identity. Protect private keys at the OS level, monitor expiration before rotation, include intermediates correctly, test new certificates before reload, and never downgrade verification merely to make a certificate-name error disappear.

11. Cleanup and checkpoint

shell · stop and remove disposable TLS lab
pg_ctl -D ./ch20_tls_pgdata stoprm -rf ./ch20_tls_pgdata ./ch20_tls_certs

Check your understanding

  1. What security property does sslmode=require lack compared with verify-full?
  2. Why does verify-full using 127.0.0.1 fail in this lab?
  3. What does pg_stat_ssl prove?
  4. Why are server.key permissions important?
  5. What must you test after certificate reload?
Review the answers

require encrypts but does not require full host identity verification. The certificate contains DNS:localhost but no matching IP SAN. pg_stat_ssl proves negotiated TLS/protocol/cipher state, not application authorization. The private key must be protected because it proves server identity. After reload, make a new verify-full connection and verify certificate identity/dates plus server logs.

Authoritative references

Authentication, TLS, authorization, and policy behavior is security- and version-sensitive. The lesson uses these PostgreSQL 18 primary sources.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.