Reported symptoms
A reporting service is deployed at 09:40 and cannot reach the analytics database:
FATAL: pg_hba.conf rejects connection for host "10.44.9.12", user "reporting_svc", database "analytics", SSL encryption
An engineer appends a rule for that role and network, reloads, and
retries. Identical message. Appends a broader rule for the whole
10.44.0.0/16. Identical message. Appends a rule using all for both
database and user. Identical message.
Other services on the same subnet connect to the same database without
trouble, including one deployed a week earlier. The reporting service
connects successfully to staging, whose pg_hba.conf was copied from
production eight months ago.
At 10:20, forty minutes in, somebody proposes restarting PostgreSQL because “the reload must not be taking effect”.
Evidence provided
$ psql -c "SELECT rule_number, line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules ORDER BY rule_number;" rule_number | line_number | type | database | user_name | address | auth_method
-------------+-------------+------+-------------+------------------+-------------+---------------
6 | 96 | host | {all} | {reporting_svc} | 10.44.0.0 | reject
12 | 141 | host | {analytics} | {reporting_svc} | 10.44.9.0 | scram-sha-256
13 | 142 | host | {analytics} | {reporting_svc} | 10.44.0.0 | scram-sha-256
14 | 143 | host | {all} | {all} | 10.44.0.0 | scram-sha-256Illustrative output
The server log for each attempt:
$ grep -A1 'rejects connection' /var/log/postgresql/postgresql-18-main.log | tail -22026-08-28 10:18:32.104 UTC [8812] reporting_svc@analytics FATAL: pg_hba.conf rejects connection for host "10.44.9.12", user "reporting_svc", database "analytics", SSL encryption
2026-08-28 10:18:32.104 UTC [8812] reporting_svc@analytics DETAIL: Connection matched file "/etc/postgresql/18/main/pg_hba.conf" line 96: "host all reporting_svc 10.44.0.0/16 reject"Illustrative output
Rule 6 was added four months earlier by a security review. The ticket describes it as “temporary pending a decision on reporting access”.
Work the evidence before reading on
- The client message is
pg_hba.conf rejects connection, notno pg_hba.conf entry. What is the difference, and what does it tell you before you look at anything else? - Three rules were appended and none changed the outcome. What single fact about the file explains that?
- Rule 14 was added to force a match. What else did it do?
- Staging works. Why is that not evidence about the client?
Root cause
First match wins, and the first match was a reject
pg_hba.conf is evaluated top to bottom. The server takes the incoming
connection’s type, database, user and address, walks the rule list, and
stops at the first rule where all four match. That rule’s method is the
verdict.
Rule 6 matched. Type host, database all, user reporting_svc,
address inside 10.44.0.0/16. Its method is reject. The server refused
and stopped.
Rules 12, 13 and 14 were loaded and valid, and were never consulted for this connection. They sit below a rule that already matched.
The escalation widened access without fixing anything
Each append was broader than the last, on the theory that a sufficiently permissive rule would eventually take effect. Rule 14 is the end of that progression:
host all all 10.44.0.0/16 scram-sha-256
Every role, every database, from a /16. It did not help the reporting service and it granted access to every other role on the estate from that entire network range.
Staging worked because staging is four months stale
The staging file was copied before the security review. It has no rule 6. That is not evidence that the client is correctly configured; it is evidence that the two files differ, which is its own finding.
Resolution
Read the DETAIL line. It names line 96. That is the whole diagnosis and
it was available at 09:42.
Remove the three rules added during the incident, starting with rule 14. They are ineffective and one of them is a real widening of access.
sed -i '141,143d' /etc/postgresql/18/main/pg_hba.conf
psql -c "SELECT pg_reload_conf();"
psql -c "SELECT rule_number, line_number, user_name, auth_method
FROM pg_hba_file_rules ORDER BY rule_number;"
Then decide what rule 6 should be. This is a policy question and it needs the security review’s owner, not an engineer at 10:30. The three outcomes:
- The reject is correct. The reporting service should not have this access and the deployment was the mistake. Close the incident against the deployment.
- The reject is stale. The decision it was waiting for was made informally and never recorded. Remove it.
- The reject is too broad. It was meant to protect specific
databases and was written with
all. Narrow it.
Whichever it is, make the change at rule 6’s position. If the service is to be allowed, either the reject goes or an allow rule goes above it.
Verification
The reporting service connects, as itself:
PGPASSWORD=... psql -h db-prod-01 -U reporting_svc -d analytics -c "SELECT 1;"
pg_hba_file_rules shows the intended rule set with the incident’s rules
gone. The count is back to eleven plus one deliberate change.
A deliberately failed attempt produces a DETAIL line naming the rule
you expect. This is the check that confirms your mental model of the file
matches the server’s.
SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL returns
zero.
No rule in the final file grants all on all from a network range.
Prevention
Put the DETAIL line at the top of the “cannot connect” runbook. It
is available by default, it names the deciding rule, and it converts a
forty-minute investigation into a thirty-second one.
Forbid appending as a diagnostic step. The only time appending helps is the one case you can rule in or out first.
Generate the file from configuration management, so that adding a
rule means choosing a position, and so that a diff showing all/all
gets seen in review.
Give temporary rules an expiry and an owner. Rule 6 was temporary four months and one outage ago.
Test the rule set. A staging cluster loaded with the production file plus a script that attempts each expected connection turns rule ordering into something you find in CI instead of at 09:40.