You waste 30 minutes because you read the error in the wrong order
In an incident, the two most common misdiagnosis patterns are:
- You see
FATAL: no pg_hba.conf entry for host ...and spend 30 minutes chasing firewalls and security groups. - You see
could not connect to server: Connection refusedand openpg_hba.confto add0.0.0.0/0 trust.
Both cases pick the wrong layer. A PostgreSQL connection has to pass the following three stages in order.
- TCP reachability — Does the client packet actually arrive at the socket postmaster is listening on?
- pg_hba rule match — Is there a line that matches the connection (TYPE/DB/USER/source IP)?
- Authentication — Do the credentials pass using the METHOD on the matched line?
The important fact: each stage produces a different message. The error text itself already tells you which stage failed. If you got Connection refused, the server has never even seen your request, so editing pg_hba.conf is 100% pointless. If you got password authentication failed, a pg_hba line already matched, so further pg_hba edits are a waste of time.
This post covers PostgreSQL 12–17 on Linux (Debian/Ubuntu and RHEL-family) plus Docker/Kubernetes/managed DB environments, and maps error text → layer → first command in 30 seconds.
Error-text lookup table: map six messages onto three layers
Find the error you are looking at in the table below. Run only that row’s “first check command.”
| # | Error text | Layer | First check command | Common wrong turn |
|---|---|---|---|---|
| (a) | psql: error: could not connect to server: Connection refused ... Is the server running on host "10.0.3.10" and accepting TCP/IP connections on port 5432? | Network | ss -lntp | grep 5432 | Editing pg_hba.conf (pointless) |
| (b) | FATAL: no pg_hba.conf entry for host "10.0.3.51", user "app", database "prod", SSL off | pg_hba | SELECT * FROM pg_hba_file_rules; | Checking firewalls/security groups |
| (c) | FATAL: no pg_hba.conf entry for host "10.0.3.51", user "app", database "prod", SSL on | pg_hba | Same + check hostnossl lines | “Do I need to turn SSL off?” rabbit hole |
| (d) | FATAL: password authentication failed for user "app" | Auth | SELECT rolname, substring(rolpassword,1,4) FROM pg_authid; | Re-editing pg_hba (already passed) |
| (e) | psql: error: FATAL: Peer authentication failed for user "app" | Auth + local socket | psql -h 127.0.0.1 -U app -d prod | Repeatedly resetting the password |
| (f) | FATAL: sorry, too many clients already | Session slots (out of scope) | SHOW max_connections; | Network/pg_hba/auth already passed |
A few interpretation rules to lock in.
(a) Network layer. The server process is down, listen_addresses is localhost, or a firewall/security group/container port mapping is blocking you. If the same symptom appears on local loopback, follow the general debug procedure in connection refused / ECONNREFUSED 127.0.0.1 30-second diagnostic runbook first. This post covers only PostgreSQL-specific items.
(b) and (c): SSL off / SSL on are not the cause — they are a record of how the client connected. The server is only stating “this connection was plaintext/TLS.” Interpretation is therefore the opposite of what people assume.
SSL off→ the client connected in plaintext, but the server only hashostssllines. → Give the clientsslmode=require, or add ahostline.SSL on→ the client connected with TLS, but you only havehostnossl, or the ADDRESS CIDR does not include the real source IP.
In both cases the conclusion is the same: “no matching line exists.” The difference is which TYPE of line you need to create.
(d) is proof that pg_hba already passed. Auth was attempted with the matched line’s METHOD (scram-sha-256/md5) and failed, so the only remaining branches are ① wrong password, ② hash-algorithm mismatch, or ③ the role does not exist.
(e) is connecting without -h, so you took the Unix-socket path. peer auth requires the OS account name and the DB role name to be the same. If switching to psql -h 127.0.0.1 changes the symptom, that is an immediate confirmation.
(f) is out of scope for this post. One diagnostic fact: this message means you already passed network, pg_hba, and authentication. For cause and recovery, see PostgreSQL too many clients already 30-second diagnosis and recovery runbook and PostgreSQL 'too many clients already': 5-minute diagnosis through a PgBouncer fix.
Three-layer diagnostic sequence: pin the failure with six commands
Once the table has narrowed the layer, run the following fixed sequence in order. Each command includes expected healthy output and the branch if it differs.
Step 1 — Check the listen socket with ss -lntp (run on the server)
sudo ss -lntp | grep 5432Expected output (remote connections possible):
LISTEN 0 244 0.0.0.0:5432 0.0.0.0:* users:(("postgres",pid=1234,fd=7))
LISTEN 0 244 [::]:5432 [::]:* users:(("postgres",pid=1234,fd=7))Branches:
- Only
127.0.0.1:5432is shown → remote access is confirmed impossible.listen_addresses='localhost', and no amount of pg_hba editing will stop error (a). - No lines at all → the server is down or on another port.
systemctl status postgresql/journalctl -u postgresql -n 50.
Step 2 — Read the running process’s actual values
Do not eyeball config files; always query the live values. Changing listen_addresses and only reloading does not apply it, so file vs. runtime mismatch is common.
SHOW listen_addresses;
SHOW port;
SHOW hba_file;
SHOW config_file;Expected output:
listen_addresses
------------------
*
port
------
5432
hba_file
-------------------------------------
/etc/postgresql/16/main/pg_hba.confBranch: if the hba_file path is not the file you edited, that itself is the cause. Package installs use /etc/postgresql/<major>/main/, source builds and RHEL-family use /var/lib/pgsql/<major>/data/, and the official Docker image uses /var/lib/postgresql/data/pg_hba.conf.
Step 3 — Attempt a real connection from remote
psql "host=10.0.3.10 port=5432 user=app dbname=prod sslmode=prefer" -c 'select 1'Take the error text from this attempt and plug it back into the table in section 2. From this point on you are judging from the table, not guessing.
Step 4 — Confirm the actual source IP recorded in the server log
ALTER SYSTEM SET log_connections = on;
SELECT pg_reload_conf();Log example:
2026-08-25 10:12:33 KST [2311] LOG: connection received: host=10.0.7.88 port=51422
2026-08-25 10:12:33 KST [2311] FATAL: no pg_hba.conf entry for host "10.0.7.88", user "app", database "prod", SSL offBranch: if the IP the server logged is not the client IP you think you have, an LB/NAT/sidecar rewrote the source IP. Jump straight to “Environment-specific branches” below. When debugging IP-based rules, the IP in the log is the truth.
Step 5 — Inspect parsed rules via SQL (PG 10+)
SELECT line_number, type, database, user_name, address, netmask, auth_method, error
FROM pg_hba_file_rules
ORDER BY line_number;Expected output:
line_number | type | database | user_name | address | netmask | auth_method | error
-------------+---------+----------+-----------+-----------+---------------+----------------+-------
89 | local | {all} | {all} | | | peer |
92 | host | {all} | {all} | 127.0.0.1 | 255.255.255.0 | scram-sha-256 |
95 | host | {prod} | {app} | 10.0.3.0 | 255.255.255.0 | scram-sha-256 |Branch: if the error column is not NULL, that line was not loaded. Typical causes are typos, a bad CIDR, or a nonexistent METHOD. Make a habit of running:
SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;pg_hba.conf syntax and first-match: one broad reject line can disable everything
Line syntax:
TYPE DATABASE USER ADDRESS METHOD [OPTIONS]Meaning of TYPE values and how they connect to errors (b) and (c):
| TYPE | Matches | Related error |
|---|---|---|
local | Unix domain socket (no -h) | (e) Peer authentication failed |
host | TCP, both plaintext and TLS | Can fix both (b) and (c) |
hostssl | TLS connections only | Connecting with SSL off produces (b) |
hostnossl | Plaintext connections only | Connecting with TLS produces (c) |
ADDRESS uses CIDR (10.0.3.0/24, 10.0.3.51/32); keywords all, samehost, and samenet are also allowed.
First-match-wins rule
PostgreSQL scans the file from the top, stops at the first matching line, and authenticates with that line’s METHOD. On failure it does not fall through to later lines. If you do not know this rule, you write a file like the one below and then spend hours on “I clearly added a line, why doesn’t it work?”
Before — a file where the lower line is ignored forever:
# TYPE DATABASE USER ADDRESS METHOD
local all all peer
host all all 127.0.0.1/32 scram-sha-256
host all all 0.0.0.0/0 reject # ← all remote connections terminate here
host prod app 10.0.3.0/24 scram-sha-256 # ← unreachable (dead rule)After — specific allows first, catch-all reject last:
# TYPE DATABASE USER ADDRESS METHOD
local all all peer
host all all 127.0.0.1/32 scram-sha-256
# Change 1: allow only the app-server subnet, TLS required (move the specific rule up)
hostssl prod app 10.0.3.0/24 scram-sha-256
# Change 2: explicitly block plaintext for the same target
hostnossl prod app 10.0.3.0/24 reject
# Change 3: catch-all reject must be the last line (moved from the old 3rd line)
host all all 0.0.0.0/0 rejectTo spot dead rules by eye, compare pg_hba_file_rules line_number order against the rule above. Treat every allow line that sits below a broad reject as invalid.
METHOD decision table
| METHOD | Security level | Client compatibility | Recommended use |
|---|---|---|---|
trust | None (no auth) | All | Forbidden in production. Bootstrap only, and always ticket it |
peer | Medium (trust OS account) | local only | Local server maintenance (postgres account) |
ident | Medium | Needs an ident server | Legacy internal networks; not recommended for new setups |
md5 | Low (legacy hash) | All, including old JDBC/psycopg2 | Temporary use for old-driver compatibility |
scram-sha-256 | High | PG 10+ server, modern drivers | Default recommendation |
cert | Very high (mTLS) | Requires distributing client certs | Internet-facing / regulated environments |
PG 14 branch: you switched to scram but error (d) keeps happening
Starting with PostgreSQL 14, the default password_encryption is scram-sha-256. The catch: existing user passwords are still stored as md5 hashes. If you only change pg_hba to scram-sha-256, the stored hash and the auth method disagree and you get password authentication failed.
Diagnose:
SELECT rolname, substring(rolpassword, 1, 4) AS hash_prefix
FROM pg_authid
WHERE rolcanlogin;Expected output:
rolname | hash_prefix
----------+-------------
postgres | SCRA
app | md5 ← this row is the causeRecover (reset the password so it is rehashed):
SET password_encryption = 'scram-sha-256';
ALTER USER app WITH PASSWORD 'new_password';Verify — re-query until hash_prefix is SCRA, then test a real connection:
psql "host=10.0.3.10 user=app dbname=prod sslmode=require" -c 'select current_user'SET is session-scoped, so for future accounts check password_encryption in postgresql.conf. If you still have old JDBC (below 9.4.12) or other drivers that do not support SCRAM, upgrading the driver is the real fix; md5 is only a temporary workaround.
Version differences
| Version | Default hash | pg_hba-related features |
|---|---|---|
| 9.6–13 | md5 | No include directive; pg_hba_file_rules from 10 |
| 14 | Switched to scram-sha-256 | Existing md5 users must be rehashed |
| 15·16·17 | scram-sha-256 | include·include_if_exists·include_dir, regex matching (/^app_.*) |
Regex matching example (PG 15+):
hostssl prod "/^app_.*" 10.0.3.0/24 scram-sha-256Environment-specific branches: Docker, Kubernetes, managed DB
| Environment | What the source IP actually is | Key settings | Classic trap |
|---|---|---|---|
| Docker | Container IP on the bridge network (172.17.0.0/16, etc.) | listen_addresses='*' + -p 5432:5432 | 127.0.0.1 inside the container is the container itself. POSTGRES_HOST_AUTH_METHOD=trust rewrites pg_hba wholesale at init |
| Kubernetes | Pod CIDR, not the Service ClusterIP | Confirm with kubectl cluster-info dump | grep -i cidr, then put that CIDR in | Sidecar/proxy hops rewrite the source IP to 127.0.0.1 |
| RDS / Cloud SQL | VPC internal IP | pg_hba cannot be edited → security groups / authorized networks + rds.force_ssl=1 parameter group | Wasting time looking for pg_hba. Access control is replaced by SGs |
Confirm source IPs in Docker:
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}'Confirm Pod CIDR in Kubernetes:
kubectl cluster-info dump | grep -i -m2 'cluster-cidr'
# expected: --cluster-cidr=10.244.0.0/16Put the confirmed range into pg_hba as-is.
hostssl prod app 10.244.0.0/16 scram-sha-256Applying changes: what reload is enough for vs. what needs a restart
| Item | reload | restart |
|---|---|---|
Entire pg_hba.conf | ✅ | Not needed |
pg_ident.conf | ✅ | Not needed |
log_connections, log_min_duration_statement | ✅ | Not needed |
listen_addresses | ❌ | ✅ Required |
port | ❌ | ✅ Required |
max_connections | ❌ | ✅ Required |
shared_buffers | ❌ | ✅ Required |
Run reload:
SELECT pg_reload_conf(); pg_reload_conf
----------------
tOr from the shell:
sudo -u postgres pg_ctl reload -D /var/lib/pgsql/16/data
# or
sudo systemctl reload postgresqlConfirm application with both of these:
sudo tail -n 20 /var/log/postgresql/postgresql-16-main.log | grep -i sighup
# expected: LOG: received SIGHUP, reloading configuration filesSELECT line_number, address, auth_method FROM pg_hba_file_rules WHERE error IS NULL;If you got t but the new line is missing from pg_hba_file_rules, the file you edited is not the path from SHOW hba_file;.
Still broken after the fix: failure-branch tree
Branch ① psql works for me, but the app keeps failing
The connection pool is holding old connections or old settings. With HikariCP, connections are not recreated until maxLifetime (default 30 minutes) expires.
spring:
datasource:
hikari:
max-lifetime: 900000 # 15 minutes
keepalive-time: 300000Confirmation is simple. If restarting the app makes it succeed immediately, the cause is pool cache. On the server, check who is actually connected:
SELECT client_addr, usename, state, backend_start
FROM pg_stat_activity
WHERE datname = 'prod'
ORDER BY backend_start DESC LIMIT 10;Branch ② traffic goes through pgbouncer
In this case the real gate is not pg_hba but pgbouncer’s auth settings.
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c 'SHOW CONFIG;' | grep -E 'auth_type|auth_file'auth_type | scram-sha-256
auth_file | /etc/pgbouncer/userlist.txtIf the hash in userlist.txt disagrees with the DB’s rolpassword, no amount of pg_hba editing will help. After PG 14 scram migrations, forgetting to update this file at the same time is an especially common accident.
"app" "SCRAM-SHA-256$4096:...$...:..."Branch ③ the source IP in the server log is unfamiliar
An LB, NAT, or service-mesh sidecar rewrote the source IP. You have two options.
- Put the CIDR of the IP that appeared in the log into pg_hba as-is (fastest, but the range may get wider).
- If you need to preserve the real source IP, look at proxy protocol or IP-preservation options on the proxy layer.
At that point IP-based rules are effectively useless, so it is more realistic to move control to auth strength (scram-sha-256 or cert) plus forced TLS.
Safety principles — lines you must not cross even when you are in a hurry
# Never-do combination
host all all 0.0.0.0/0 trusttrust+0.0.0.0/0is unauthenticated, fully open access. Never leave it in production under any circumstances.- Keep CIDRs as tight as possible:
/32if you can, otherwise the application subnet. - Block plaintext explicitly with
hostsslallow +hostnossl ... reject. - Temporary relaxations must be ticketed with an expiry date. A leftover temporary
trustline that sits around for years is the incident type reported most often in practice.
For official docs, see the “Client Authentication” chapter in the PostgreSQL manual (pg_hba.conf File, Authentication Methods) together with the pg_hba_file_rules view description.
Conclusion: three-layer connection-error checklist
| Error-text keyword | Layer | First command |
|---|---|---|
Connection refused ... 5432 | Network | ss -lntp | grep 5432 |
no pg_hba.conf entry ... SSL off/on | pg_hba | SELECT * FROM pg_hba_file_rules; |
password authentication failed | Auth | SELECT rolname, substring(rolpassword,1,4) FROM pg_authid; |
Peer authentication failed | Auth (local socket) | Retry with psql -h 127.0.0.1 ... |
too many clients already | Session slots | See the separate runbook |
Two sentences to remember.
- Run
ss -lntpbefore you open pg_hba.conf. - If you got
password authentication failed, pg_hba already passed.
Run the two lines below on your own server right now and check for latent errors. It is the cheapest way to find dead rules and parse errors before an incident.
SHOW hba_file;
SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;If you got this far and connections are still blocked, the remaining candidate is connection-slot exhaustion. Continue with PostgreSQL too many clients already 30-second diagnosis and recovery runbook.
FAQ
Q1. I got no pg_hba.conf entry ... SSL off. Should I turn SSL off on the server?
No. SSL off is not the cause; it is a record that this connection was plaintext. You most likely only have hostssl lines on the server, so the correct response is to add sslmode=require to the client connection string, or add an appropriate host line on the server.
Q2. I edited pg_hba.conf but the change is not applying.
Check three things in order. ① SHOW hba_file; — is the live path the file you edited? ② Does SELECT pg_reload_conf(); return t? ③ Does SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL; show any parse errors? Note that changing listen_addresses and port requires a restart, not a reload.
Q3. Where do I edit pg_hba.conf on RDS or Cloud SQL?
You cannot edit pg_hba.conf on managed databases. Access control is done with security groups / authorized networks, and TLS enforcement with parameter-group settings such as rds.force_ssl=1. Auth strength is managed via password_encryption and resetting role passwords; confirm exact parameter names in each cloud vendor’s official docs.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.