Postgres 'no pg_hba.conf entry for host': the scoped fix, not 0.0.0.0/0
You add a new app server, point it at the database, and the deploy dies on the first query. Or you move Postgres into Docker, everything looks right, and the connection is flatly refused with a message about a config file you have never opened. It is one of the most common Postgres errors there is, and it has a fix so fast that most people reach for the dangerous one before they understand what the database is actually telling them.
The error is not a bug and it is not a password problem. Postgres has a firewall built into it, described by a plain text file called pg_hba.conf, and that firewall has decided the combination of who you are, where you are connecting from, and which database you want is not on the list. The two-minute fix that circulates everywhere is to add a rule allowing everything from everywhere. That makes the error disappear and quietly turns your database into an open port on the internet. This is the article for doing it properly.
Postgres rejected the connection because no line in pg_hba.conf matches your host, database, and user together. Add a rule scoped to the actual client, not a blanket one:
# TYPE DATABASE USER ADDRESS METHOD
host appdb appuser 10.0.4.17/32 scram-sha-256
Then reload without a restart: SELECT pg_reload_conf();. If the client cannot even reach the port, also set listen_addresses in postgresql.conf and restart. Never paste 0.0.0.0/0 trust to make it go away.
The exact error, verbatim
The message comes back to the client the moment the TCP connection is made, before any query runs:
FATAL: no pg_hba.conf entry for host "10.0.4.17", user "appuser", database "appdb", no encryption
Older Postgres versions end the line with SSL off instead of no encryption; both mean the same thing. Drivers wrap it in their own class name, so you may see it as:
org.postgresql.util.PSQLException: FATAL: no pg_hba.conf entry for host "10.0.4.17", user "appuser", database "appdb", SSL off
psycopg2.OperationalError: connection to server at "db" (172.19.0.2), port 5432 failed: FATAL: no pg_hba.conf entry for host
Every word after host matters. Postgres is handing you the exact four facts it used to search the rule file: the source IP, the user, the database, and whether the connection was encrypted. The fix is to make a rule that matches all four.
Why this happens: the file is a firewall, read top to bottom
pg_hba.conf (HBA stands for host-based authentication) is a list of rules. When a connection arrives, Postgres walks the file from the top and stops at the first line whose type, database, user, and address all match the incoming connection. That line decides the authentication method. If it reaches the bottom without a match, you get the FATAL error. There is no implicit allow.
Three things trip people up here.
First match wins, so order matters. A narrow reject line above a broad allow line will reject. A broad line above a narrow one means the narrow one is never read. If you add your new rule at the bottom of the file, a matching line higher up may already be answering (and rejecting) the connection.
The address is checked as a CIDR range, not a hostname. A rule for 10.0.4.0/24 covers 10.0.4.17; a rule for 127.0.0.1/32 does not cover anything except the loopback. The IP Postgres reports in the error is the one it saw after any NAT, which in container and cloud setups is frequently not the address you think you are connecting from.
Connection type must match too. A line beginning hostssl only matches encrypted connections; hostnossl only matches unencrypted ones; a plain host matches either. This is why an error can say the host and user are exactly what your rule allows and still fail: your rule says hostssl and the client connected in the clear, so the line was skipped. That trailing no encryption is the clue.
The fix, in numbered steps
Step 1: Find which pg_hba.conf is actually in use
There can be more than one on disk. Ask the running server which file it loaded, so you edit the right one:
SHOW hba_file;
-- /etc/postgresql/16/main/pg_hba.conf
On Debian and Ubuntu the file lives under /etc/postgresql/<version>/main/. In the official Docker image it is /var/lib/postgresql/data/pg_hba.conf inside the volume. Editing a copy that the server never reads is the most common reason a correct rule seems to do nothing.
Step 2: Add a scoped rule for the real client
Take the four facts straight out of the error message and write one line that matches them. Put it above any broader lines so it is reached first:
# TYPE DATABASE USER ADDRESS METHOD
host appdb appuser 10.0.4.17/32 scram-sha-256
If the app runs on a known subnet rather than one fixed IP, use the CIDR for that subnet, for example 10.0.4.0/24, and no wider. Prefer scram-sha-256 as the method; it is the modern password mechanism and the default in current Postgres. Use md5 only if you still have pre-scram clients, and never trust, which skips authentication entirely.
Step 3: Reload, do not restart
Changes to pg_hba.conf take effect on a reload. No connections are dropped:
SELECT pg_reload_conf();
-- t
Or from the shell: sudo systemctl reload postgresql, or pg_ctl reload -D /path/to/data. In Docker, docker exec <container> kill -HUP 1 reloads the server without stopping it.
Step 4: If the port itself is unreachable, fix listen_addresses
The HBA file only decides who is allowed once they reach the server. It does not open the network socket. If the connection is refused before you even see the pg_hba error, the server is not listening on that interface. That is set separately, in postgresql.conf:
# postgresql.conf
listen_addresses = 'localhost,10.0.4.5' # or '*' to listen on all interfaces
Changing listen_addresses does require a restart, because it changes which sockets the server binds at startup. Listening on * is fine as long as pg_hba.conf and your real firewall still control who gets through.
Verification: prove the rule works
Connect from the actual client machine, not from the database host, using the same user and database as the error:
psql "host=10.0.4.5 port=5432 dbname=appdb user=appuser" -c "select current_user, inet_client_addr();"
# current_user | inet_client_addr
# --------------+------------------
# appuser | 10.0.4.17
The inet_client_addr() value is the exact IP Postgres matched your rule against. If it is not what you expected (a NAT gateway, a Docker bridge address), that is the address your CIDR needs to cover. To see which line answered a connection, set log_connections = on and watch the log; recent versions can log the matched pg_hba line and its number, which removes all guesswork about ordering.
What people get wrong
The advice you will find in a hundred threads is: add host all all 0.0.0.0/0 md5 (or worse, trust) and reload. It works, in the sense that the error stops. It also means every database on that server now accepts a login attempt from every IP address on the planet, protected only by a password, and with trust not even that. Automated scanners find open 5432 ports within hours. This is not a theoretical risk; it is how a lot of small teams get their data dumped.
The second mistake is reaching for hostssl ... 0.0.0.0/0 thinking TLS makes the wildcard safe. Encryption protects the connection in transit; it does nothing about who is allowed to open it. A scoped host rule for a /32 or a small /24 is both safer and simpler than a wildcard with TLS bolted on.
The third is treating this as the same class of problem as too many clients already or connection refused from a container. Those are capacity and networking problems. This one is purely authorisation: the packet arrived, Postgres looked you up, and you were not on the list.
When it is still broken
If the correct-looking rule still fails, work through these:
- You edited the wrong file. Re-run
SHOW hba_file;and confirm the path. In Docker, confirm you edited the file inside the mounted volume, not a stale copy baked into an image layer. - A broader line above yours is matching first. Move your rule up. A
rejectline or a wide match earlier in the file will answer before Postgres ever reaches your addition. - The IP in the error is not the client's real IP. Behind NAT, a load balancer, or a container bridge, Postgres sees the translated address. Scope your CIDR to what
inet_client_addr()reports, or to the Docker bridge subnet (often inside172.16.0.0/12). - You reloaded but the process did not pick it up. Confirm the reload returned
tand check the server log for a syntax warning; a malformed line makes Postgres keep the old rules and log a complaint rather than fail loudly.
Get into the habit of writing the narrow rule the first time. It takes ten extra seconds to type the real subnet instead of 0.0.0.0/0, and it is the difference between a database that is reachable by your app and one that is reachable by everyone.
Frequently asked questions
- Do I need to restart Postgres after editing pg_hba.conf?
- No. Changes to pg_hba.conf only need a config reload, not a restart. Run SELECT pg_reload_conf(); as a superuser, or pg_ctl reload, or systemctl reload postgresql. A restart is only required when you change listen_addresses in postgresql.conf, because that opens a new listening socket.
- What does 'SSL off' or 'no encryption' at the end of the error mean?
- It is Postgres telling you which connection type it tried to match. If your matching rule is hostssl but the client connected without TLS, no line matches and you get this error even though the address looks allowed. Use a plain host line to match both, or fix the client to connect with sslmode=require.
- Is adding host all all 0.0.0.0/0 md5 a safe fix?
- No. That line accepts a password login for every database and every user from any IP address on the internet, which is exactly the exposure you do not want on a database. Scope the rule to the specific client CIDR, the specific database, and the specific user, and rely on scram-sha-256 rather than md5.
- Why does my app work from the host but not from a Docker container?
- Containers connect from the Docker bridge network, not from 127.0.0.1. A localhost-only pg_hba rule will reject them. Add an explicit rule for the bridge subnet (commonly 172.16.0.0/12) or connect over the compose service network, and make sure listen_addresses covers the interface the container reaches.