Fix PostgreSQL SSL Connection Errors in Django
Fix Django PostgreSQL TLS errors: no pg_hba.conf entry with no encryption, server does not support SSL, root.crt missing for verify-full, certificate verify failed, and SSL SYSCALL EOF drops.
Problem statement
Django cannot connect to PostgreSQL, and the error mentions SSL or encryption:
FATAL: no pg_hba.conf entry for host "10.0.1.20", user "myapp", database "myapp", no encryption
server does not support SSL, but SSL was required
root certificate file "/home/myapp/.postgresql/root.crt" does not exist
SSL error: certificate verify failed
SSL SYSCALL error: EOF detected
These look similar but have different causes: the server requires TLS and the client did not use it, the client requires TLS and the server does not offer it, the client is asked to verify the server's certificate but has no CA to verify against, or an established TLS connection was cut. This guide maps each message to its fix and shows the Django settings for each sslmode.
no pg_hba.conf entry ... no encryption: the server only accepts TLS; setsslmode=require(or stricter) in Django.server does not support SSL: the client demands TLS but the server has it disabled; enable TLS on the server, or usesslmode=preferonly on a trusted private network.root certificate file ... does not exist:verify-ca/verify-fullneeds a CA bundle; setsslrootcertto the provider's CA file (orsystemwith libpq 16 and newer).certificate verify failed: wrong CA bundle, hostname mismatch (verify-fullrequires the host you connect to match the certificate), or an expired certificate.SSL SYSCALL error: EOF detected/SSL connection has been closed unexpectedly: the connection was dropped mid-session; enable TCP keepalives,CONN_HEALTH_CHECKS, and check idle timeouts on firewalls and poolers.
Step-by-step solution
1. Configure sslmode in Django
Pass libpq connection parameters through OPTIONS:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "myapp",
"USER": "myapp",
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": "db.example.internal",
"PORT": "5432",
"OPTIONS": {
"sslmode": "verify-full",
"sslrootcert": "/etc/ssl/certs/db-ca.pem",
},
}
}
With dj-database-url, add query parameters instead: postgres://...:5432/myapp?sslmode=verify-full&sslrootcert=/etc/ssl/certs/db-ca.pem.
What the modes mean:
| sslmode | Encrypts | Verifies CA | Verifies hostname |
|---|---|---|---|
disable | no | no | no |
prefer (libpq default) | if the server supports it | no | no |
require | yes | no | no |
verify-ca | yes | yes | no |
verify-full | yes | yes | yes |
require protects against passive eavesdropping; only verify-full protects against an attacker impersonating the server. Use verify-full for databases reached over networks you do not fully control.
2. "no pg_hba.conf entry ... no encryption"
The server has a hostssl rule for your client (or requires TLS globally, as managed services often do) and the client connected without TLS. Common reasons: sslmode=disable somewhere, a DATABASE_URL without sslmode combined with a client library built without SSL support, or a proxy in between that strips TLS.
Set sslmode=require or stricter. On a self-managed server, check the rules:
sudo grep -E "^\s*host" /etc/postgresql/16/main/pg_hba.conf
hostssl rules match only TLS connections; hostnossl only non-TLS; host both. Amazon RDS for PostgreSQL 15 and newer enforces TLS by default through the rds.force_ssl parameter.
3. "server does not support SSL, but SSL was required"
The client asked for TLS and the server has ssl = off. On a self-managed server, enable it with a certificate and key:
# postgresql.conf
ssl = on
ssl_cert_file = '/etc/postgresql/tls/server.crt'
ssl_key_file = '/etc/postgresql/tls/server.key'
The key must be readable only by the postgres user. Reload PostgreSQL. Inside a private Docker network between two containers on one host, running without TLS may be an accepted trade-off; then use sslmode=disable explicitly rather than require.
4. "root certificate file ... does not exist"
With verify-ca or verify-full, libpq needs the certificate authority that signed the server's certificate. If sslrootcert is not set, it looks for ~/.postgresql/root.crt in the home directory of the user running Django, which usually does not exist.
- Download the CA bundle your provider publishes (for example the RDS global bundle, or the CA certificate from your managed database's connection page), store it on the server or in the image, and point
sslrootcertat it. - With libpq 16 or newer (check
python -c "import psycopg; print(psycopg.pq.version())"),sslrootcert=systemuses the operating system's trusted CAs, which works for providers whose certificates are signed by public CAs.
5. "certificate verify failed"
- Wrong CA: the bundle does not contain the CA that signed the server certificate. Providers rotate CAs; update the bundle.
- Hostname mismatch (
verify-full): you connect by IP address or an internal alias that is not in the certificate. Connect using the hostname the certificate is issued for. - Expired certificate on a self-managed server: renew it and reload PostgreSQL.
Inspect what the server presents:
openssl s_client -starttls postgres -connect db.example.internal:5432 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
The -starttls postgres option requires OpenSSL 1.1.1 or newer.
6. "SSL SYSCALL error: EOF detected"
This happens on an existing connection: something closed it without a TLS goodbye. Usual causes are idle connections killed by a firewall, NAT gateway, or load balancer; the database restarting or failing over; or a pooler's idle timeout. Django reusing a persistent connection (CONN_MAX_AGE) that was silently dropped then fails on the next query.
DATABASES["default"].update({
"CONN_MAX_AGE": 60,
"CONN_HEALTH_CHECKS": True,
})
DATABASES["default"]["OPTIONS"].update({
"keepalives": 1,
"keepalives_idle": 60,
"keepalives_interval": 10,
"keepalives_count": 5,
})
CONN_HEALTH_CHECKS makes Django verify a reused connection at the start of each request and reconnect if it is dead. Keepalives stop idle-timeout devices from dropping the connection. Keep CONN_MAX_AGE below the shortest idle timeout on the path.
Verification check:
.venv/bin/python manage.py dbshell -- -c "SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
Expect ssl = t with a modern TLS version. For verify-full, also confirm that connecting by a different name (for example the IP address) fails, which proves hostname verification is active.
Explanation
libpq, the C library under psycopg, negotiates TLS before authentication. sslmode controls two independent decisions: whether to encrypt, and how much to trust the server's certificate. Servers enforce their side through pg_hba.conf rules and ssl settings. Most errors come from the two sides disagreeing (one requires what the other does not offer) or from the client being asked to verify without the material it needs (a CA bundle and a matching hostname).
Edge cases / notes
Connection poolers
With PgBouncer between Django and PostgreSQL, there are two TLS connections: client to PgBouncer (client_tls_* settings) and PgBouncer to server (server_tls_*). An error may come from either hop.
Client certificates
Some setups authenticate the client with a certificate (sslcert, sslkey options). The key file must be readable only by the user running Django, or libpq refuses it.
When to automate this
Ship the CA bundle with your image or provisioning, keep sslmode in the same configuration source for every environment, and add a startup or CI check that queries pg_stat_ssl to confirm connections are encrypted.
Next steps
- Set up managed databases correctly with Connect Django to Managed PostgreSQL Securely.
- For non-TLS connection problems, see Fix Database Connection Errors in Django Production.
- Tune connection reuse with Configure Persistent Database Connections in Django.
- If TLS works but login fails, see Fix PostgreSQL "password authentication failed" for Django.
FAQ
Which sslmode should Django use in production?
verify-full with the provider's CA bundle when connecting over networks you do not fully control. require encrypts but does not protect against server impersonation.
Why does libpq look for root.crt in my home directory?
With verify-ca or verify-full and no sslrootcert, libpq defaults to ~/.postgresql/root.crt. Set sslrootcert explicitly.
How do I check whether Django's database connection is encrypted?
Query pg_stat_ssl for the current backend: SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid();.
What causes "SSL SYSCALL error: EOF detected"?
An established connection was closed unexpectedly, often by an idle timeout on a firewall, load balancer, or pooler. Use keepalives and CONN_HEALTH_CHECKS.