Postgres pg_hba.conf: How Authentication Actually Gets Decided
The file that decides who can connect, from where, and how they prove who they are. Most production incidents involving Postgres auth come from misreading the order of evaluation.
Postgres has a file that decides, for every incoming connection, whether to accept it and how to authenticate the user. The file is pg_hba.conf, the hba stands for host-based authentication, and it is one of the most consequential configuration files in a typical Postgres deployment. The file is also one of the most frequently misread, because it is evaluated top to bottom with the first matching row winning, which means the order of rows changes behavior in ways that are not obvious from looking at any single row.
What the file does
pg_hba.conf is consulted on every new connection attempt, before the database has any user-level authorization logic. The file consists of records, one per line, with five fields: connection type (local, host, hostssl, hostnossl, hostgssenc, hostnogssenc), database name (or all), user name (or all), client address (CIDR or hostname, ignored for local), and authentication method (trust, reject, md5, scram-sha-256, peer, ident, cert, gss, sspi, ldap, pam, radius, bsd).
The evaluation rule is simple in principle: the server walks the records top to bottom, and the first record whose connection type, database, user, and address all match is used. The authentication method specified by that record determines whether the connection is accepted, rejected, or requires further authentication. If no record matches, the connection is rejected.
The complication is that most production pg_hba.conf files contain a mix of permissive and restrictive rules that interact in ways that depend on ordering. A common mistake is adding a new rule at the bottom of the file expecting it to take effect, when an earlier rule already matches. Another common mistake is putting a restrictive rule below a more permissive one and being surprised when the restrictive rule never fires.
The authentication methods that matter
The list of methods is long, but in practice production deployments use a small subset. trust accepts the connection with no authentication, which is appropriate only for local Unix-socket connections from the same machine where the OS layer is providing trust (and even then, only sometimes). scram-sha-256 is the modern password-based method, replacing the older md5 method which should be considered deprecated for new deployments. peer uses the operating system user identity for Unix-socket connections, which is the right default for local connections from named OS users to same-named database users. cert uses TLS client certificates, which is the right default for service-to-service connections in environments with certificate infrastructure. gss uses Kerberos, which appears mostly in large enterprise environments.
The choice of method has security and operational consequences. scram-sha-256 stores password verifiers using a salted hash with iteration count, so a compromised database snapshot does not directly reveal passwords. md5 stores a salted MD5 hash that is much weaker against offline attack. The migration from md5 to scram-sha-256 requires updating the password_encryption server parameter and having users re-set their passwords, which is a non-trivial operational project for an existing deployment but is the right thing to do.
The order problem
The first matching rule wins. This means that a typical configuration with a permissive rule for local connections followed by restrictive rules for remote connections works correctly only because the local connections never reach the remote rules. Add a hostssl all all 0.0.0.0/0 scram-sha-256 line at the top and suddenly every connection in the system uses that rule, including ones you intended to authenticate differently.
The diagnostic for ordering bugs is to read the file as if you were the server: for a given (type, database, user, address) tuple, find the first matching row. The match rules are: connection type matches type, database matches database (or database is "all"), user matches user (or user is "all"), address matches address (or it is a local connection where address is ignored). The authentication method is then the action taken for that connection.
One subtlety: the database and user fields can be comma-separated lists, or can use the special "all", or can be the name of a group role (prefixed with +) whose members all match. The address field can be a CIDR block, a hostname (resolved at connection time, which is slow and is usually wrong), or the special "samehost" or "samenet".
The reject keyword
The reject method is underused. Most pg_hba.conf files achieve restrictive behavior by relying on the no-match-rejects fallthrough, which works but produces uninformative error messages on the client side. An explicit reject rule earlier in the file allows you to fail certain connections immediately with a clear log entry, which is useful for diagnosing why a connection that should not happen is being attempted.
The pattern is: put a reject rule above any permissive rule that might otherwise match. For example, if you want to allow a specific subnet but explicitly deny a known-bad address within that subnet, put the reject rule for the specific address above the allow rule for the subnet. The narrowest rule has to come first, because the first match wins.
SSL and the host variants
The five host variants (host, hostssl, hostnossl, hostgssenc, hostnogssenc) let you require or forbid encryption at the connection level. host matches any TCP connection regardless of encryption status. hostssl matches only TLS-encrypted connections. hostnossl matches only unencrypted connections. The hostgssenc and hostnogssenc variants are analogous for GSSAPI encryption, which is rare outside Kerberos environments.
The right default for any production deployment exposing Postgres to a network is to require hostssl for every external connection and use hostnossl only for explicit local-network exceptions that have been thought through. The simplest enforcement is to put hostnossl all all 0.0.0.0/0 reject as the first rule for non-local connections, which forces all subsequent rules to apply only to TLS connections without having to remember to write hostssl on every row.
The change-and-reload mechanics
pg_hba.conf is read on server start and on SIGHUP. Changes do not take effect until reload, which can be triggered by pg_ctl reload or by calling pg_reload_conf() from any superuser session. Reload does not affect existing connections, which continue with the authentication method they used at connection time. This means that revoking access for a compromised account requires both updating pg_hba.conf and terminating existing connections from that account.
The pg_hba_file_rules view (added in Postgres 10) shows the parsed rules with line numbers and any parse errors, which is the right way to verify changes before reloading. Reading the raw file and trying to predict which rules will match is harder than it sounds, and the view does the parsing for you.
Common misconfigurations
The most common pg_hba.conf bugs in production deployments share a small set of patterns. The first is leaving the default local all all trust rule in place, which means anyone who can get shell access on the database server can connect as the postgres superuser without a password. The default should be changed to peer or scram-sha-256 on any production deployment.
The second is using host instead of hostssl for remote connections, which means an attacker who can MITM the connection can downgrade it to unencrypted and capture credentials. The fix is to use hostssl for all remote rules and explicitly reject hostnossl as the first non-local rule.
The third is using broad CIDR ranges (10.0.0.0/8, 0.0.0.0/0) without thinking through what other hosts are in that range. A 10.0.0.0/8 rule that was intended for a specific application server's network actually covers 16 million addresses, which probably includes networks you do not want.
The fourth is forgetting to update pg_hba.conf when adding a new database or user, with the result that the new database or user inherits whatever rule happens to match "all". The right pattern is explicit rules for important databases and users, with "all" rules only at the bottom as fallback.
What pg_hba.conf does not do
pg_hba.conf decides whether to accept a connection and how to authenticate. It does not decide what the authenticated user can do once connected; that is the job of the database's permissions system (GRANT, REVOKE, role membership, row-level security). A user authenticated via pg_hba.conf can still be denied access to specific tables or operations by the database's authorization layer.
pg_hba.conf also does not provide rate limiting or connection limits. Those are handled by other configuration (max_connections globally, per-role connection limits via ALTER ROLE, external rate limiting in PgBouncer or similar). A pg_hba.conf rule that accepts a connection does not promise that the server has capacity to handle it; that is a separate problem.
Our use across the four products
DocuMint, CronPing, FlagBit, and WebhookVault all run on SQLite, which has no pg_hba.conf equivalent because there is no network connection to authenticate; access is controlled by filesystem permissions on the database file. The Postgres equivalent would be deciding which Unix users on the server can read the file, which is much narrower than network-based authentication.
When we eventually migrate the products that need it from SQLite to Postgres (likely WebhookVault and FlagBit first, based on workload characteristics), pg_hba.conf becomes one of the load-bearing configuration files. The pattern will be: hostssl-only for external connections, scram-sha-256 with per-product database users, certificate-based authentication for service-to-service traffic, and explicit reject rules for connection patterns that should never occur. Most of this is conventional Postgres practice, but the conventional practice exists for reasons that pg_hba.conf bugs in less-careful deployments make visible.
The deeper observation is that the most consequential configuration files in a typical infrastructure are the ones that decide who can do what. pg_hba.conf is small (often under 50 lines), simple in syntax, evaluated by a well-understood algorithm, and the wrong setting can produce either an unreachable database or one open to the internet. The discipline is reading the file as the server reads it, not as the file appears visually, and using pg_hba_file_rules to verify changes before reloading.