Skip to main content
RunBook Academy

PostgreSQLV · Authentication, Roles and TLSAuthentication

pg_hba.conf: first match wins

Intermediate⏱ ~30 min🧪 Lab requiredpsql

What you'll learn

  • Read a pg_hba.conf line and state exactly which connections it matches
  • Predict the outcome when several rules could match one connection
  • Use the server log to identify which rule actually matched
  • Change the rule set without locking anybody out

Prerequisites

Practice

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

pg_hba.conf is not a policy engine. It is an ordered list, and the server stops reading at the first line that matches the connection in front of it. That line decides the authentication method, and if that method fails or the line says reject, the connection is refused. No later line is consulted.

Almost every surprising access outcome in PostgreSQL follows from that single sentence.

The columns

# TYPE   DATABASE   USER         ADDRESS          METHOD    [OPTIONS]
host     all        demo_scram   127.0.0.1/32     scram-sha-256
ColumnMatches
TYPElocal (Unix socket), host (any), hostssl (TLS only), hostnossl (non-TLS only)
DATABASEA name, all, replication, a comma list, or @file
USERA role name, all, +groupname for members of a role, or a list
ADDRESSA CIDR, a hostname, all, samehost, samenet
METHODtrust, reject, scram-sha-256, md5, peer, cert, ldap, gss, oauth, and others

All four of the first columns must match for the line to apply. +groupname is worth noting: it matches any role that is a member of that role, which is how you write rules against a group rather than enumerating individuals.

First match wins, demonstrated

Two experiments on the same live cluster, differing only in the order of two lines.

Order A — the broad rule above the specific one:

local   all   all                       trust
host    all   all         127.0.0.1/32  reject
host    all   demo_scram  127.0.0.1/32  scram-sha-256
Read-only / Safethe specific rule never runs
$ PGPASSWORD=... psql -h 127.0.0.1 -U demo_scram -d postgres -c 'SELECT 1'
psql: error: connection to server at "127.0.0.1", port 5432 failed:
FATAL:  pg_hba.conf rejects connection for host "127.0.0.1", user "demo_scram",
      database "postgres", no encryption

Order B — the same three rules, specific one first:

local   all   all                       trust
host    all   demo_scram  127.0.0.1/32  scram-sha-256
host    all   all         127.0.0.1/32  reject
Read-only / Safethe same connection now succeeds
$ PGPASSWORD=... psql -h 127.0.0.1 -U demo_scram -d postgres -tAc 'SELECT current_user'
demo_scram

Same rules, same connection, opposite outcomes. That is the whole lesson, and it is why reasoning about this file by eye is unreliable once it exceeds a handful of lines.

Making the evaluation observable

Two mechanisms remove the guesswork.

The connection log names the matching line. With log_connections enabled, PostgreSQL records the file and line number of the rule that authenticated the connection:

Read-only / Safethe log names file and line of the rule that matched
$ docker logs rbpg-auth | grep 'connection authenticated'
LOG:  connection authenticated: user="postgres" method=trust (/var/lib/postgresql/18/docker/pg_hba.conf:2)
LOG:  connection authenticated: identity="demo_scram" method=scram-sha-256 (/var/lib/postgresql/18/docker/pg_hba.conf:2)

pg_hba_file_rules shows the parsed rule set, in order, with any parse errors, without reloading:

psql -U postgres -c \
  "SELECT rule_number, line_number, type, database, user_name,
          address, auth_method, error
     FROM pg_hba_file_rules ORDER BY rule_number"

The error column is the important one. It reports a line the server could not parse before you reload, which turns a potential lockout into a query result. Checking it is the single most valuable habit in this lesson.

Here it is catching a real mistake — a peer method on a host line, which cannot work because a TCP connection carries no operating-system identity:

Read-only / Safethe check finds the fault while the server still runs the old rules
$ psql -U postgres -c 'SELECT line_number, type, auth_method, error FROM pg_hba_file_rules WHERE error IS NOT NULL'
 line_number | type | auth_method |                         error
-------------+------+-------------+--------------------------------------------------------
         4 |      |             | peer authentication is only supported on local sockets
(1 row)

Note that type and auth_method are empty for a line that failed to parse: the server could not interpret it, so it has nothing to report about it beyond the error and the line number. An empty result from this query is the pass condition.

Changing the rules safely

pg_hba.conf is re-read on reload, so changes are cheap to apply and cheap to get wrong.

# 1. Keep a copy you can restore in one step
cp "$(psql -U postgres -tAc 'SHOW hba_file')" /var/backups/pg_hba.conf.$(date -u +%Y%m%dT%H%M%SZ)

# 2. Edit, then check the parse BEFORE reloading
psql -U postgres -c \
  "SELECT line_number, type, database, user_name, address, auth_method, error
     FROM pg_hba_file_rules WHERE error IS NOT NULL"

# 3. Reload
psql -U postgres -c 'SELECT pg_reload_conf()'

# 4. Confirm the server accepted it
psql -U postgres -c \
  "SELECT count(*) AS rules_with_errors FROM pg_hba_file_rules WHERE error IS NOT NULL"

# 5. Test the connection you changed, from where it actually comes from

Step 2 is what prevents a lockout: pg_hba_file_rules reads the file from disk and reports parse errors while the server is still running the previous, working rule set.

What the official container image ships

Worth knowing, because it is what most people meet first:

Read-only / Safethe default pg_hba.conf in the official postgres image
$ grep -vE '^#|^$' $PGDATA/pg_hba.conf
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
local   replication     all                                     trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust
host    all             all             all                     scram-sha-256

Anything arriving over the loopback interface or the Unix socket is trusted without a password. Connections from elsewhere need SCRAM.

For a disposable lab container that is a sensible default. It becomes a problem when the same image is used for something long-lived and another process on the same host — or another container sharing the network namespace — can reach the loopback address.

Production discipline

  1. Order the file from most specific to most general. A broad rule above a specific one makes the specific one dead code.
  2. Check pg_hba_file_rules for errors before every reload. It reads the file while the server still runs the working rule set.
  3. Enable log_connections so the matching rule is recorded. The file and line number settle disputes that otherwise become arguments.
  4. Keep a local peer rule as the safety rope, so a mistake in the network rules does not lock you out of the host.
  5. Never widen the method to diagnose. Widen the address on a rule that still authenticates, and only temporarily.
  6. Copy the file before editing it. Restoring is then one cp and a reload.

Cross-course references

  • Secrets, PKI & Certificate Management — Part VII (TLS for operators) covers what hostssl actually requires of the client, and Part XI (SSH) covers the analogous ordered-rule reasoning in sshd_config.
  • Linux for Production Sysadmins — Part XXV (Firewall) covers the same first-match-wins semantics in nftables, and Part XXVI (SSH) covers keeping an out-of-band route in before changing access rules.
  • Ansible for Production Sysadmins — Part XVII (Templates) covers generating this file from a source of truth, which is how the ordering stops being accidental.

Quiz

Knowledge check · 6 questions

  1. Q1. A pg_hba.conf contains a reject rule for 10.0.0.0/8 on line 4 and a scram-sha-256 rule for the same range on line 9. A client from 10.2.3.4 connects. What happens?

  2. Q2. Which check should be run after editing pg_hba.conf but before reloading?

  3. Q3. Which statements about pg_hba.conf are correct? Select all that apply.

  4. Q4. The official postgres container image ships a pg_hba.conf that trusts connections from the loopback address without a password.

  5. Q5. Distinguish the errors 'pg_hba.conf rejects connection for host' and 'no pg_hba.conf entry for host', and say what each calls for.

  6. Q6. Explain the failure and give the safe remediation sequence.

    A change added monitoring access by inserting a line at the top of pg_hba.conf reading 'host all all 10.0.0.0/8 trust', then reloading. Monitoring began working. Three weeks later a security review finds that four application services which had been configured for scram-sha-256 are connecting without presenting a password, and that a decommissioned host still holds valid connectivity. The applications have reported no errors at any point.

Passing score: 75%. Answers are checked in this browser.