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.

TLS between Django and PostgresDjango OPTIONSsslmode, sslrootcertlibpqTLS handshakeServer certificatesigned by a CApg_hba.confhostssl rulessslmode decides whether TLS is used and how much of the server certificate is verified.
  1. no pg_hba.conf entry ... no encryption: the server only accepts TLS; set sslmode=require (or stricter) in Django.
  2. server does not support SSL: the client demands TLS but the server has it disabled; enable TLS on the server, or use sslmode=prefer only on a trusted private network.
  3. root certificate file ... does not exist: verify-ca/verify-full needs a CA bundle; set sslrootcert to the provider's CA file (or system with libpq 16 and newer).
  4. certificate verify failed: wrong CA bundle, hostname mismatch (verify-full requires the host you connect to match the certificate), or an expired certificate.
  5. 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",
        },
    }
}
sslmode and the CA file"OPTIONS": {"sslmode": "verify-full","sslrootcert": "/etc/ssl/certs/rds-global-bundle.pem",},121Encrypt, check the CA, check the hostname.2The provider’s CA bundle, readable by the app user.require encrypts but does not verify who you are talking todjango-deployment.com
The same keys can go in DATABASE_URL as query parameters.

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:

sslmodeEncryptsVerifies CAVerifies hostname
disablenonono
prefer (libpq default)if the server supports itnono
requireyesnono
verify-cayesyesno
verify-fullyesyesyes

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"

SSL error messages, decodedErrorCauseno pg_hba.conf entry … no encryptionserver requires SSL; set sslmodeserver does not support SSLSSL off on the server, sslmode=requireroot certificate file does not existverify mode without sslrootcertcertificate verify failedwrong CA bundle or hostname mismatchMatch the message exactly; each has one usual causedjango-deployment.com
The sections below take each message in turn.

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 sslrootcert at it.
  • With libpq 16 or newer (check python -c "import psycopg; print(psycopg.pq.version())"), sslrootcert=system uses 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.

the connection died while idleQuery succeedsconnection keptIdle for minutesCONN_MAX_AGENAT or LB drops itsilentlyNext querySSL SYSCALL error: EOFFixhealth checks, keepalivesCONN_HEALTH_CHECKS replaces a dead connection before it is useddjango-deployment.com
keepalives_idle in OPTIONS keeps the path from timing out.
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

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.

Keep going