DEV Community

Philip McClarence
Philip McClarence

Posted on

No pg_hba.conf Entry for Host: 5 Real Causes & Fixes

I've been paged for this error at 2am more times than I care to count. I've also watched a well-meaning junior "fix" it at 2:07am by appending host all all 0.0.0.0/0 trust to a cluster with a public IP, then going back to bed. The connection worked. So did everyone else's.

πŸ“– Read the full guide: Fix "no pg_hba.conf entry for host" (Full Cause Map)

No pg_hba.conf Entry for Host: 5 Real Causes & Fixes

Here's the short version: no pg_hba.conf entry for host means your TCP connection reached Postgres fine β€” the failure is at the authentication-matching step, not the network step. Postgres tells you exactly which rule it needed, in the server log, on one line. Most advice you'll find skips reading that line and jumps straight to editing the file.

TL;DR β€” the 60-second triage

  • Read the server log line, not the client error. It names the host address, user, database, and encryption state Postgres matched against. That's your entire diagnosis.
  • Confirm where the server is listening: SHOW listen_addresses; and ss -ltnp 'sport = :5432'. (If you got the pg_hba error at all, this is already fine β€” more on that below.)
  • Read the file as Postgres parsed it: SELECT * FROM pg_hba_file_rules; β€” check the error column before you trust anything.
  • Add the narrowest rule that matches, then reload: SELECT pg_reload_conf(); No restart needed for pg_hba.conf changes.

What this error actually means (and what it doesn't)

pg_hba is the second gate, not the first. The sequence is:

  1. TCP connect to the postmaster β€” governed by listen_addresses, the OS firewall, cloud security groups, and routing.
  2. The postmaster matches the incoming connection against pg_hba.conf and either authenticates or refuses.

If you're seeing no pg_hba.conf entry for host, stage one already succeeded. The client opened a TCP session, the postmaster read the startup packet, and the server decided it had nothing to say to you. Your firewall is fine. listen_addresses is fine. The port is open.

When those are broken, you get a completely different failure β€” a postgres remote connection error that looks like this:

psql: error: connection to server at "db01" (10.0.3.10), port 5432 failed:
        Connection refused
        Is the server running on that host and accepting TCP/IP connections?
Enter fullscreen mode Exit fullscreen mode

or a hang followed by a timeout. Neither of those is an HBA problem, and neither will ever produce the pg_hba error text. Roughly half the answers I read online respond to "no pg_hba.conf entry" with "did you set listen_addresses = '*'?" If that mattered, the asker wouldn't have gotten this error in the first place.

Read the log line before you touch anything

Here's the thing you actually need, straight out of the server log:

2026-08-04 02:14:07.118 UTC [21873] FATAL:  no pg_hba.conf entry for host "10.0.3.42", user "app", database "orders", no encryption
Enter fullscreen mode Exit fullscreen mode

Decompose it:

Field Value What it tells you
host 10.0.3.42 The source address the server sees. Not what the client thinks it is.
user app The role from the startup packet, after any client-side mapping.
database orders The requested database. Note: not all, not the default.
encryption no encryption The connection isn't SSL, so hostssl lines can't match it.

That last field is the one people skim past. no encryption means a hostssl rule will never match this connection no matter how perfect the CIDR is. If it said SSL on, a hostnossl rule is excluded instead.

The 10.0.3.42 is the single most useful fact in the whole investigation, and it's the value you should be grepping your pg_hba.conf for β€” not the client's ip addr output. The server's view.

Root cause 1: Postgres isn't listening where you think

This is the pre-error case β€” you never get the HBA message, you get "Connection refused."

SHOW listen_addresses;
SHOW hba_file;
Enter fullscreen mode Exit fullscreen mode

listen_addresses defaults to localhost in a source build. * binds all interfaces, 0.0.0.0 binds all IPv4, :: binds all IPv6, and an empty string disables TCP entirely (Unix sockets only). Confirm at the OS level:

$ ss -ltnp 'sport = :5432'
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      244        127.0.0.1:5432       0.0.0.0:*     users:(("postgres",pid=1041,fd=6))
Enter fullscreen mode Exit fullscreen mode

Bound to loopback only. No amount of pg_hba editing will help.

Two traps here. First, listen_addresses requires a full restart, not a reload β€” it's a postmaster-level parameter. Second, 0.0.0.0 covers IPv4 only; if your client resolves the hostname to an AAAA record you'll try to connect over IPv6 to nothing, and vice versa with ::.

Also check SHOW hba_file; before you edit anything. On Debian and Ubuntu packages it's /etc/postgresql/16/main/pg_hba.conf, not inside $PGDATA, and I have absolutely watched someone edit the wrong file for twenty minutes.

Root cause 2: no rule matches at all

A host record has five fields:

TYPE DATABASE USER ADDRESS METHOD
host orders app 10.0.3.0/24 scram-sha-256

Postgres reads the file top to bottom and the first matching record wins. There's no fall-through and no backup. If the first matching rule's authentication fails, the connection fails β€” even if a more permissive rule sits three lines below. Placement matters as much as content.

Verify the file parsed the way you think, via pg_hba_file_rules:

SELECT line_number, type, database, user_name, address, netmask, auth_method, error
FROM pg_hba_file_rules;
Enter fullscreen mode Exit fullscreen mode
 line_number | type  |   database    | user_name |  address  |    netmask     |  auth_method   | error
-------------+-------+---------------+-----------+-----------+----------------+----------------+-------
          89 | local | {all}         | {all}     |           |                | peer           |
          91 | host  | {all}         | {all}     | 127.0.0.1 | 255.255.255.255| scram-sha-256  |
          93 | host  | {all}         | {all}     | ::1       | ffff:...:ffff  | scram-sha-256  |
          97 | host  | {orders}      | {app}     | 10.0.1.5  |                |                | invalid CIDR mask in address "10.0.1.5/24"
Enter fullscreen mode Exit fullscreen mode

That error column is why this view exists. Non-null means the line didn't parse, and Postgres is still running on whatever was loaded before.

Root cause 3: the rule matches the host but not the rest

The subtle one, and the one that costs the most time.

all doesn't include replication

The all keyword in the database column matches every database except physical replication connections. If your standby is failing to connect and your log shows database "replication", you need an explicit line:

host    replication     repl    10.0.3.0/24    scram-sha-256
Enter fullscreen mode Exit fullscreen mode

I've seen a failover rehearsal die on exactly this, with a perfectly good host all all 10.0.3.0/24 scram-sha-256 sitting right there. Logical replication connects to a named database and is matched by all; physical replication is not.

Connection type must match the encryption state

host matches both encrypted and unencrypted TCP. hostssl matches SSL-only. hostnossl matches non-SSL only. hostgssenc and hostnogssenc do the same for GSSAPI encryption. Cross-reference with the encryption clue in the log line β€” if it says "no encryption" and your only matching line is hostssl, Postgres skips past it as if it weren't there.

Role membership needs a +

A user column of app matches only the role literally named app. +app_users matches any member of app_users, directly or indirectly. @filename reads a list from an include file.

Method mismatch after PG14

Since PostgreSQL 14, password_encryption defaults to scram-sha-256. An md5 HBA line can still authenticate a role whose password is stored as a SCRAM hash β€” Postgres negotiates the right protocol underneath. But a scram-sha-256 line cannot authenticate a role still holding an old MD5 hash. It's a one-way door: once you migrate a password to SCRAM, the md5 line becomes unnecessary, but tightening a line to scram-sha-256 before every password is rotated will lock out anyone who hasn't reset theirs yet. Rotate the password, then tighten the line.

peer is local-only

It matches the OS username to the role name over Unix sockets. On a host line it's invalid and will never authenticate anyone.

You can also use reject deliberately, high in the file, to shut a subnet out regardless of what follows.

Root cause 4: CIDR arithmetic and address family

The most common typo I see is /32 where someone meant /24, or vice versa β€” a /32 matches exactly one address, and writing it when you meant a subnet locks out everyone except the one IP you happened to test with.

CIDR IPv4 hosts covered IPv6 equivalent IPv6 hosts covered
/32 1 (single host) /128 1 (single host)
/24 256 /64 typical LAN subnet
/16 65,536 /48 typical site allocation
/0 everything /0 everything (never use this)

In concrete terms:

Notation Covers
10.0.3.42/32 exactly that one IPv4 host
10.0.3.0/24 10.0.3.0 – 10.0.3.255
10.0.0.0/16 10.0.0.0 – 10.0.255.255
0.0.0.0/0 every IPv4 address on earth
::1/128 exactly that one IPv6 host
fd00:dead:beef::/64 that IPv6 subnet

Two failure modes. First, non-zero bits to the right of the mask are an error in modern versions: 10.0.1.5/24 gets flagged, not silently rounded to 10.0.1.0/24. pg_hba_file_rules.error will show it.

Second, an IPv4 entry doesn't match an IPv6 connection. 127.0.0.1/32 won't authorize a client arriving from ::1. That's why the shipped default file has both lines. If your app host resolves localhost to ::1, you need the IPv6 entry too.

samehost and samenet are decent shorthands: they match the server's own addresses and the subnets it's directly attached to.

Root cause 5: containers, NAT, and proxies

The address in the log is the last hop, not the original client β€” this is the classic pg_hba.conf docker connection refused scenario in disguise.

Docker bridge networks NAT everything through the gateway. Find the real numbers:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.Gateway}} {{.IPAddress}}{{end}}' app_web_1
172.18.0.1 172.18.0.7
Enter fullscreen mode Exit fullscreen mode

Write your rule against 172.18.0.0/16, or the compose network's subnet, and be aware that this range is shared with anything else on that bridge. In Kubernetes, the pod CIDR is not the node IP β€” decide which one you're actually seeing before writing anything, and match your rule to what the log line says, not to what you assume the topology looks like.

PgBouncer and HAProxy collapse every client into a single source address. Your HBA rule authorizes the pooler; user-level control has to move into the pooler's own auth configuration.

The official postgres Docker image ships a permissive host rule and exposes POSTGRES_HOST_AUTH_METHOD to set it. Setting it to trust disables password checking for all host connections. Fine for a throwaway test container, unacceptable anywhere else. Set it to scram-sha-256 and provide a password.

Sanity check the exact path the server sees:

$ psql "host=10.0.3.10 port=5432 user=app dbname=orders sslmode=disable"
Enter fullscreen mode Exit fullscreen mode

Applying changes safely: reload vs restart

pg_hba.conf takes effect on SIGHUP. No restart, no dropped sessions:

SELECT pg_reload_conf();
Enter fullscreen mode Exit fullscreen mode
$ pg_ctl -D $PGDATA reload
Enter fullscreen mode Exit fullscreen mode

If the file has a syntax error at reload time, the server logs it and keeps the previously loaded rules. That's a feature, and it's also why pg_hba_file_rules (which reads the file on disk) can disagree with what's actually enforced. Always check:

SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;
Enter fullscreen mode Exit fullscreen mode

Empty result, then reload, then retest. listen_addresses and port still need a full restart.

The lines I actually use

# TYPE  DATABASE      USER        ADDRESS           METHOD

# local admin via unix socket, OS user must match role
local   all           all                           peer

# loopback: both families, always both
host    all           all         127.0.0.1/32      scram-sha-256
host    all           all         ::1/128           scram-sha-256

# application subnet, scoped to one db and one role
host    orders        +app_users  10.0.3.0/24       scram-sha-256

# physical replication: 'all' above does NOT cover this
hostssl replication   repl        10.0.3.0/24       scram-sha-256

# admin access from the bastion only, single host, SSL required
hostssl all           dbadmin     10.0.9.15/32      scram-sha-256
Enter fullscreen mode Exit fullscreen mode

Never trust. Never 0.0.0.0/0. Not temporarily. "Temporary" HBA lines are the most durable objects in our industry, and the incident review is worse than the outage.

Audit before someone locks you out (or lets everyone in)

SELECT line_number, type, database, user_name, address, auth_method,
       CASE
         WHEN auth_method = 'trust'                    THEN 'CRITICAL: no authentication'
         WHEN address IN ('0.0.0.0','::')              THEN 'CRITICAL: open to the world'
         WHEN auth_method = 'md5'                      THEN 'WARN: legacy md5, move to scram-sha-256'
       END AS finding
FROM pg_hba_file_rules
WHERE auth_method IN ('trust','md5')
   OR address IN ('0.0.0.0','::')
ORDER BY line_number;
Enter fullscreen mode Exit fullscreen mode

Run that on every cluster you own. Then look for shadowing: because first match wins, a broad rule near the top silently disables every narrower rule below it, and nothing warns you.

This is the check I got tired of writing by hand on every cluster. MyDBA runs this class of check continuously and flags rules that shadow or over-permit β€” that's the whole pitch.

If you'd rather see this walked through on a whiteboard, the companion video is up at https://www.youtube.com/@pgdba. Otherwise, you now have everything you need to fix a no pg_hba.conf entry for host error without guessing.

Tags: postgres database devops security ## One line to remember

Every one of these five failure modes produces the exact same eleven words on the client. The server log doesn't lie, and it doesn't summarize β€” it hands you host, user, database, and encryption state on a plate. Read that line first, every time, before you touch a single CIDR block or auth method. Nine times out of ten the fix is one narrow line added in the right place, reloaded, and retested. The tenth time is the one where someone already added trust at 2am, and now you're doing an access audit instead of a quick config fix.

pg_hba.conf isn't complicated once you stop treating it as a firewall and start treating it as what it is β€” a first-match ordered list of authentication contracts. Keep it narrow, keep it explicit about SSL and role membership, and check pg_hba_file_rules after every change instead of trusting that a reload did what you meant it to.

pgdba Editorial builds MyDBA, a Postgres monitoring and health-check tool β€” https://mydba.dev/?utm_source=devto&utm_medium=platform&utm_campaign=no-pg-hba-conf-entry-for-host-fix

If you're maintaining more than a couple of clusters, it's worth having something watch your pg_hba rules for shadowing and over-permissive entries so you're not the one finding out at 2am. That's what MyDBA is for β€” give it a look.

Top comments (0)