This post was originally published in July 2022, and we’ve updated it in 2025 for clarity and relevance, reflecting current practices while honoring their original perspective.
Protecting data in transit is just as important as protecting it at rest. If connections to your PostgreSQL database aren’t encrypted, sensitive information like usernames, passwords, and SQL queries can be intercepted. The good news is that enabling SSL/TLS in PostgreSQL is straightforward, and with a few additional steps, you can make sure encryption is not just available, but enforced.
In this blog, we’ll walk through how to set up SSL/TLS for PostgreSQL, how to enforce its usage through configuration, and how to confirm that your connections are truly secure.
What is SSL/TLS?
SSL (Secure Sockets Layer) and TLS (Transport Layer Security) are cryptographic protocols that encrypt communications between a client and server. TLS is the modern version, but many people still use “SSL” as shorthand.
Without encryption, anyone with access to the network could capture PostgreSQL traffic and view sensitive data. Enabling SSL/TLS ensures your connections remain private and compliant with security requirements.
For further detail, the PostgreSQL documentation includes dedicated sections on Secure TCP/IP Connections with SSL and SSL Support entries for more information.
Trying to enable SSL without Cert/Key Files
Let’s now see what happens when we try to enable SSL without having the needed certificate and key files in place:
1 2 3 4 5 6 7 |
postgres=# alter system set ssl=on; ALTER SYSTEM postgres=# select pg_reload_conf(); pg_reload_conf ---------------- t (1 row) |
We don’t see any errors, but are we really using SSL? If we check the error log, we’ll indeed see the errors:
1 2 3 4 |
2022-06-23 20:43:54.713 UTC [5284] LOG: received SIGHUP, reloading configuration files 2022-06-23 20:43:54.714 UTC [5284] LOG: parameter "ssl" changed to "on" 2022-06-23 20:43:54.715 UTC [5284] LOG: could not load server certificate file "server.crt": No such file or directory 2022-06-23 20:43:54.715 UTC [5284] LOG: SSL configuration was not reloaded |
Creating certificates
So, we first need to create the aforementioned files. If you don’t already have valid certificate and key files, a quick one-liner for this is the following openssl command (it’s not the focus here to delve too much into this part of the process):
1 2 3 4 5 6 7 |
[root@node0 ~]# cd /var/lib/pgsql/14/data [root@node0 data]# openssl req -nodes -new -x509 -keyout server.key -out server.crt -subj '/C=US/L=NYC/O=Percona/CN=postgres' Generating a 2048 bit RSA private key ....+++ .........................+++ writing new private key to 'server.key' ----- |
We have changed the current working directory to the PostgreSQL data directory since we were in a RHEL-based system. If you are on a Debian-based one, you should store the files in /etc/ssl/certs/ and /etc/ssl/private/ or define/check ssl_cert_file and ssl_key_file PostgreSQL configuration variables, respectively. Also, make sure the postgres user owns them, and they are only readable to it:
1 2 3 4 5 |
[root@node0 data]# chmod 400 server.{crt,key} [root@node0 data]# chown postgres:postgres server.{crt,key} [root@node0 data]# ll server.{crt,key} -r--------. 1 postgres postgres 1212 Jun 23 20:49 server.crt -r--------. 1 postgres postgres 1704 Jun 23 20:49 server.key |
Certificates and keys must be owned by the postgres user and locked down with proper permissions. This prevents unauthorized access and ensures PostgreSQL can use them correctly.
Enabling SSL/TLS
Now we can enable SSL and reload the configuration again; this time with no errors shown:
1 2 3 4 5 6 7 |
postgres=# alter system set ssl=on; ALTER SYSTEM postgres=# select pg_reload_conf(); pg_reload_conf ---------------- t (1 row) |
1 2 |
2022-06-23 20:52:05.823 UTC [5284] LOG: received SIGHUP, reloading configuration files 2022-06-23 20:52:05.823 UTC [5284] LOG: parameter "ssl" changed to "on" |
At this point, SSL is enabled in PostgreSQL, but that alone doesn’t mean all connections are encrypted. Unless you update your pg_hba.conf file, some clients may continue to connect without SSL. That’s where enforcement comes in.
Enforcing SSL/TLS
As mentioned, the pg_hba.conf file is where we can tune which connections are going to be required to use SSL. We can instruct PostgreSQL to enforce this by using the “hostssl” keyword instead of the plain “host” one. Note that you can see some connections starting to use SSL at this point because the plain “host” keyword will allow for connections that want to use SSL to use it. However, this is not enforcing SSL to be used (i.e.: if the client doesn’t want to use SSL, PostgreSQL will not deny the connection).
Let’s imagine this is the pg_hba.conf file we have been using so far:
1 2 3 4 5 6 |
# TYPE DATABASE USER ADDRESS METHOD local all all peer host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256 host all all 0.0.0.0/0 md5 host replication all 10.124.33.113/24 md5 |
And we want to enforce SSL connections from all remote users (and also include remote replication connections):
1 2 3 4 5 6 |
# TYPE DATABASE USER ADDRESS METHOD local all all peer host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256 hostssl all all 0.0.0.0/0 md5 hostssl replication all 10.124.33.113/24 md5 |
Again, this is not enough if we are adamant about really enforcing connections to use SSL. We have to call pg_reload_conf() once more to make sure they are loaded into PostgreSQL itself:
1 2 3 4 5 |
postgres=# select pg_reload_conf(); pg_reload_conf ---------------- t (1 row) |
At this point, new remote non-SSL connections will be denied:
1 2 |
[root@node1 ~]# psql "host=10.124.33.132 sslmode=disable" psql: error: connection to server at "10.124.33.132", port 5432 failed: FATAL: no pg_hba.conf entry for host "10.124.33.113", user "postgres", database "postgres", no encryption |
By replacing host with hostssl in your pg_hba.conf rules, you instruct PostgreSQL to require SSL for those connections. Without this change, clients can still bypass SSL if they choose.
Reload the configuration and test connections to confirm that non-SSL attempts are denied.
Checking for connections using SSL/TLS
We can check for connections using SSL with the following query:
1 2 3 4 5 6 7 8 9 10 11 |
postgres=# select pg_ssl.pid, pg_ssl.ssl, pg_ssl.version, pg_sa.backend_type, pg_sa.usename, pg_sa.client_addr from pg_stat_ssl pg_ssl join pg_stat_activity pg_sa on pg_ssl.pid = pg_sa.pid; pid | ssl | version | backend_type | usename | client_addr ------+-----+---------+----------------+----------+--------------- 5547 | f | | walsender | postgres | 10.124.33.113 5549 | f | | client backend | postgres | 10.124.33.132 5556 | f | | client backend | postgres | 10.124.33.113 (3 rows) |
In this case, the replication connection (walsender) is not yet using SSL and neither are the two other clients connected, so we need to force a restart if we want them to reconnect. As always, we recommend that you try all these steps in a testing environment first and that when it’s time to do it in production you do them in a properly established maintenance window (no matter how trivial the steps seem to be).
To force the replication connections to use SSL, one can either restart the service in the replica or use pg_terminate_backend (which will send the SIGTERM signal to the process and is safe to use in this context). In this case, we are using pg_terminate_backend in the primary itself, but it can also be used in the replica, provided we are using the correct PID number.
1 2 3 4 5 |
postgres=# select pg_terminate_backend(5547); pg_terminate_backend ---------------------- t (1 row) |
After that, we should see the new replica connection correctly using the SSL/TLS protocol:
1 2 3 4 5 6 7 8 9 10 11 |
postgres=# select pg_ssl.pid, pg_ssl.ssl, pg_ssl.version, pg_sa.backend_type, pg_sa.usename, pg_sa.client_addr from pg_stat_ssl pg_ssl join pg_stat_activity pg_sa on pg_ssl.pid = pg_sa.pid; pid | ssl | version | backend_type | usename | client_addr ------+-----+---------+----------------+----------+--------------- 5557 | t | TLSv1.2 | walsender | postgres | 10.124.33.113 5549 | f | | client backend | postgres | 10.124.33.132 5556 | f | | client backend | postgres | 10.124.33.113 (3 rows) |
PID 5549 is our own connection, so that’s an easy fix:
1 2 3 4 5 |
postgres=# select pg_backend_pid(); pg_backend_pid ---------------- 5549 (1 row) |
Connection from 5556 would be the remaining one for us to check if we need to enforce SSL on all. On the client-side, we can use conninfo to check information on our current connection:
1 2 3 |
postgres=# conninfo You are connected to database "postgres" as user "postgres" on host "10.124.33.132" at port "5432". SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off) |
Queries against pg_stat_ssl joined with pg_stat_activity let you confirm which connections are encrypted and which are not. This helps ensure your changes are working and lets you spot any clients that still need to be forced over to SSL.
Disabling SSL/TLS
If you decide to disable SSL, be careful not to lock yourself out. Always make sure pg_hba.conf allows non-SSL connections before toggling SSL off.
Conclusion
Enabling SSL/TLS in PostgreSQL is quick, but making it mandatory requires extra attention. Simply setting ssl=on won’t force encryption — you need to configure pg_hba.conf with hostssl entries and verify connections through system views.
Taking these steps helps protect sensitive data, meet compliance standards like PCI DSS, SOX, or GDPR, and reduce the risk of exposure.
If you want to learn more about securing PostgreSQL beyond SSL/TLS, check out our guide: PostgreSQL security missteps and tips.
Or, explore the Percona PostgreSQL Hub for enterprise resources, including high availability, Kubernetes deployments, and AI readiness.
Thank you for this article.
but i think i am missing the information of how the clients are connecting to the database with ssl ? dont they need to use key to login ?