Fix "no pg_hba.conf entry for host" (Full Cause Map)

no pg_hba.conf entry for host: the complete fix map

Here is the line, copied out of a customer's log last month with the hostnames changed:

<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:0.75rem;margin:2rem 0;"> <iframe src="https://www.youtube.com/embed/CD5RzhtZagI" title="FATAL: no pg_hba.conf entry for host — The Complete Fix Map" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div>

▶ Watch on YouTube: FATAL: no pg_hba.conf entry for host — The Complete Fix Map

Fix "no pg_hba.conf entry for host" (Full Cause Map)

2026-07-14 09:41:22.318 UTC [21874] FATAL:  no pg_hba.conf entry for host "172.19.0.4", user "app", database "appdb", no encryption
2026-07-14 09:41:22.318 UTC [21874] DETAIL:  Client IP address resolved to "unknown", forward lookup not checked.

Before you touch anything, notice what the server just handed you. Four facts, all of them from the server's own point of view:

| Field in the message | What it tells you | |---|---| | host "172.19.0.4" | The source address as the server sees it, after any NAT, SNAT or proxy rewriting | | user "app" | The role name in the startup packet, exactly as the client sent it | | database "appdb" | The database requested, again from the startup packet | | no encryption | The connection is plaintext TCP, so hostssl records could not have matched |

The server could only print those four things because it already accepted the TCP connection, completed (or skipped) the TLS handshake, and parsed the startup packet. Everything below the transport layer worked. What failed is rule matching: Postgres walked pg_hba.conf from the top and reached the bottom without finding a record whose connection type, database, user and address all matched this tuple at once.

That is why the fix map is short and mechanical. Read the four fields out of the error, confirm you are editing the file the running server actually uses, make one record match on all four columns including the encryption variant, reload, verify with pg_hba_file_rules.

There is a companion video that whiteboards this in about ten minutes if you prefer watching; this article is the written reference with the exact commands.

Triage first: four errors people mix up

Most of the wasted time on this error comes from treating it as a networking problem. Sort the symptom properly and you have already done 80% of the work.

| What you see | What actually happened | Where the fix lives | |---|---|---| | could not connect to server: Connection refused or a TCP timeout | Nothing is listening on that address/port, or a firewall dropped the packet. Postgres never saw you. | listen_addresses, port, host firewall, security group, k8s Service/NetworkPolicy | | FATAL: no pg_hba.conf entry for host ... | Connection accepted, startup packet parsed, no matching record found | pg_hba.conf record content and ordering | | FATAL: pg_hba.conf rejects connection for host ... | A record did match, and its auth method is reject | Find the matching reject line and move or remove it | | FATAL: password authentication failed for user "app" or authentication method 10 not supported | A record matched, authentication under it failed | Password, password_encryption, driver version |

The two on the ends get conflated constantly. Half the search results for this error open with "set listen_addresses = '*' and restart Postgres." If you are literally reading no pg_hba.conf entry for host, listen_addresses is already correct for that client, because the client got far enough to be named in the log. Restarting is the wrong first move: it costs you an outage window and changes nothing.

How Postgres actually evaluates pg_hba.conf

A record is five fields plus optional options:

# TYPE   DATABASE   USER    ADDRESS            METHOD           [OPTIONS]
host     appdb      app     10.20.4.0/24       scram-sha-256
local    all        postgres                   peer

local covers Unix-domain sockets and has no address field at all. The TCP types are host, hostssl, hostnossl, hostgssenc and hostnogssenc. host matches SSL and non-SSL connections; hostssl matches only encrypted ones.

The scan is sequential, top to bottom, and first match wins. If the matched record's authentication fails, Postgres does not fall through to try later records. This is the mechanical reason that "I appended my line to the bottom of the file and nothing changed" is such a common report. If a broad record above it already matched the tuple, your new line is dead text.

Since PostgreSQL 16 you also have include, include_if_exists and include_dir in pg_hba.conf, and pg_hba_file_rules grew a file_name column so you can see which file a rule came from. Distro packages and configuration management love dropping files into an included directory, so check there before you conclude a rule doesn't exist.

Cause 1: you are editing a file the server never reads

This is the most common wasted hour, and it takes ten seconds to rule out.

SHOW hba_file;
SHOW config_file;
SHOW data_directory;

Typical answers:

| Platform | hba_file | |---|---| | Debian/Ubuntu packages | /etc/postgresql/17/main/pg_hba.conf | | RHEL/Rocky packages | /var/lib/pgsql/17/data/pg_hba.conf | | Official Docker image | /var/lib/postgresql/data/pg_hba.conf (often bind-mounted from elsewhere) | | Patroni | rendered into the data directory from the DCS; edits on disk get overwritten |

On Debian the file is not in the data directory, which is exactly where people go looking. Under Patroni, hand edits survive until the next restart and then vanish, which produces a genuinely confusing intermittent failure.

Read the live file and the parsed rules from inside a session:

-- the bytes the server is reading
SELECT pg_read_file(current_setting('hba_file'));

-- the parsed view, including lines that failed to parse
SELECT rule_number, line_number, type, database, user_name,
       address, netmask, auth_method, error
FROM pg_hba_file_rules
ORDER BY rule_number;

Two caveats on pg_hba_file_rules: it reflects the file on disk, which may differ from the rules currently in effect if nobody has reloaded, and the error column is the fastest way to find a typo that a reload silently rejected.

Cause 2: the address doesn't match

Cause 2: the address doesn't match

Trust the IP in the log line. Not ip addr on the client, not what the developer believes their pod's address is. The server printed the address it received the packet from, after every rewriting layer in between.

A CIDR cheat table, because the arithmetic mistakes are boringly repetitive:

| Address value | Matches | Does not match | |---|---|---| | 10.20.4.7/32 | exactly 10.20.4.7 | 10.20.4.8 | | 10.20.4.0/24 | 10.20.4.0 through 10.20.4.255 | 10.20.5.1 | | 10.20.0.0/16 | 10.20.0.0 through 10.20.255.255 | 10.21.0.1 | | 0.0.0.0/0 | every IPv4 address | every IPv6 address, including ::1 | | ::/0 | every IPv6 address | IPv4 addresses | | samehost | any IP address of this server | anything else | | samenet | any address in any subnet the server is directly attached to | routed traffic from other subnets | | .internal.example.com | any host whose name ends in that suffix | anything failing the DNS check below |

The IPv4/IPv6 split catches people weekly. A client connecting to localhost on a dual-stack box arrives as ::1, and your host all all 127.0.0.1/32 record does not cover it. You need both:

host    all    all    127.0.0.1/32    scram-sha-256
host    all    all    ::1/128         scram-sha-256

The same split bites on real traffic, not just loopback. 0.0.0.0/0 is a common attempt at "allow everyone" that quietly excludes IPv6 clients, and vice versa for ::/0. If your app connects over IPv6 and your rule is IPv4-only CIDR, you get this exact FATAL with an address that looks like an IPv4-mapped literal, ::ffff:10.0.0.12, and the rule looks "right" right up until you check the address family.

You can also write the address with a separate netmask column (10.20.4.0 255.255.255.0), which is legal and occasionally still shows up in old files.

Host-name records require the client address to reverse-resolve to a name and then that name to forward-resolve back to the same address. That is two DNS round trips per connection attempt and a hard dependency on your resolver. I avoid them on anything latency-sensitive.

Then there is the rewriting layer, which is where "but the app server is 10.20.4.7!" comes from:

Cause 3: the database or user column doesn't match

Cause 3: the database or user column doesn't match

The single most surprising rule in the whole file: all in the database column does not match physical replication connections. A streaming replica, pg_basebackup, or pgBackRest fetching a backup all request the special replication pseudo-database, and only a record with replication in that column will match. You can have host all all 0.0.0.0/0 scram-sha-256 at the top of your file and still get no pg_hba.conf entry for the standby.

The line you want:

host    replication    replicator    10.0.0.0/24    scram-sha-256

Placement matters. Put it above any broad host all all ... record. It won't be shadowed by that record (because all never matches replication), but it can be shadowed by an earlier host replication all 0.0.0.0/0 reject or a catch-all reject, and keeping the narrow rules on top is the habit that keeps you out of trouble.

Other user-column behaviour worth knowing:

Before and after

The bug:

# pg_hba.conf (before)
local   all             postgres                                peer
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             10.0.0.0/8              scram-sha-256
host    replication     replicator      10.0.0.0/24             scram-sha-256   # appended, never reached? no:

The replication line here is fine. The classic broken version is the opposite: someone appends host appdb app 10.20.4.7/32 trust under an existing host all all 10.0.0.0/8 reject and wonders why nothing changed. Narrow above broad:

# pg_hba.conf (after)
local   all             postgres                                peer
host    replication     replicator      10.0.0.0/24             scram-sha-256
host    appdb           app             10.20.4.0/24            scram-sha-256
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             ::1/128                 scram-sha-256
host    all             all             10.0.0.0/8              reject

Cause 4: the SSL/encryption variant doesn't match

Go back to the trailing no encryption in the original log line. If your file contains only hostssl records for that subnet, a plaintext client gets no pg_hba.conf entry, not a TLS error. The client author then spends an afternoon debugging certificates that were never involved.

The inverse trap is hostnossl, which explicitly refuses TLS connections. A client with sslmode=require hitting a hostnossl-only setup gets the same class of error, now with SSL encryption appended to the trailing field instead of no encryption — same failure mode, opposite direction.

Client sslmode interacts directly with this:

| sslmode | Behaviour | |---|---| | disable | plaintext only; will never match a hostssl record | | prefer | tries TLS, silently falls back to plaintext, so failures move around | | require | TLS only; will never match a hostnossl record |

The managed-service case is worth stating plainly. On AWS RDS for PostgreSQL, setting rds.force_ssl = 1 makes non-SSL connections fail with exactly this error and an SSL off suffix. There is no pg_hba.conf you can edit on RDS. The fix is on the client: connect with sslmode=require or higher, or change the parameter group.

Cause 5: listen_addresses and the network layer

The correction from earlier deserves its own section, because listen_addresses genuinely belongs on the map even though it is not your bug today.

Default value is localhost. * means all interfaces; you may also give a comma-separated list such as 'localhost,10.20.4.9'. Which interface you bind determines the route packets take, and therefore the source address the server records, which is why VPN vs LAN routing can change the client IP that shows up in this error.

ss -lntp | grep 5432

And the operational difference that keeps people honest:

> listen_addresses is a postmaster-context parameter and needs a full restart. pg_hba.conf takes effect on a reload (SIGHUP, pg_ctl reload, or SELECT pg_reload_conf()).

Docker and Compose: the same five causes in costume

Nothing new happens in containers, the addresses just look unfamiliar.

localhost inside a container is that container's own network namespace. An app container connecting to localhost:5432 is looking for Postgres inside itself. Container-to-container traffic arrives from a bridge address: 172.17.0.0/16 on the default bridge, other subnets from the 172.16.0.0/12 pool for user-defined networks. Rules written for 127.0.0.1/32 never match any of it. On Docker Desktop, host.docker.internal resolves to the host.

The official postgres image's entrypoint appends host all all all <POSTGRES_HOST_AUTH_METHOD> to pg_hba.conf during first-time initialisation and configures the server to accept TCP on all interfaces, defaulting to scram-sha-256 unless you set POSTGRES_HOST_AUTH_METHOD=trust. So a stock container almost never produces this error. When it does, it is one of three things: a bind-mounted pg_hba.conf overriding the generated one, a custom network with an address range your rules don't cover, or POSTGRES_HOST_AUTH_METHOD confusion.

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: appdb
    networks: [backend]
    ports: ["127.0.0.1:5432:5432"]

  api:
    build: .
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/appdb
    depends_on: [db]
    networks: [backend]

networks:
  backend:

Test from inside the app container, which is the only test that reproduces the real source address:

docker compose exec api psql "postgres://app:$DB_PASSWORD@db:5432/appdb" -c "select inet_client_addr(), current_user, current_database();"

Apply and verify without locking yourself out

The safe change ritual:

1. Confirm local all postgres peer (or an equivalent superuser path over the socket) exists and is above anything that could shadow it. This is your way back in. 2. Take a copy: cp pg_hba.conf pg_hba.conf.$(date +%F). 3. Edit, keeping narrow records above broad ones. 4. SELECT pg_reload_conf(); 5. Verify:

SELECT rule_number, line_number, type, database, user_name,
       address, auth_method, error
FROM pg_hba_file_rules
ORDER BY rule_number;

Every row should have error IS NULL. If a line fails to parse, the reload is rejected, the error is logged, and the previously loaded rules stay in effect. That is a friendly failure mode with a nasty tail: a server start with an invalid file will not come up. A broken Friday edit that a reload quietly refused becomes a Monday outage the first time someone restarts the instance. Always check the error column after the reload, not after the next restart.

Confirm from the server side with log_connections = on, then watch the log while the client retries:

tail -f /var/log/postgresql/postgresql-17-main.log | grep -E 'no pg_hba|connection authorized'

Rules that are "working" and still wrong

A file with no failures in it can still be a problem waiting for an audit:

How MyDBA catches this before it locks anyone out

MyDBA's health checks read pg_hba_file_rules on every monitored instance. They flag lines with a non-null error column, records using trust, addresses broader than a configured threshold, instances with no matching replication record for a known standby's address, and differences in parsed rules between a primary and its replicas.

The connection-error view groups repeated FATAL: no pg_hba.conf entry messages by client address, user and database, so a deploy that rolls out with a new pod CIDR shows up as a spike with the offending IP attached rather than as a page from the on-call engineer.

MyDBA health check listing pg_hba rules flagged as overly broad, with the parse error column and primary/replica drift

Both views are read-only and use the same SQL shown above. If you want to see them against your own estate, MyDBA will run the checks on connect.

Related reading: reload vs restart for every GUC context, and monitoring authentication failures as a leading indicator of a bad deploy.