How to Tell Which pg_hba.conf Rule Allowed Your Connection in YugabyteDB

One of the most common questions when working with PostgreSQL-style authentication is:

  • “Which pg_hba.conf rule actually allowed this connection?”

If you’ve ever stared at a long HBA file wondering why a connection succeeded (or failed), you’re not alone.

YugabyteDB follows PostgreSQL’s first-match-wins HBA evaluation model, but it doesn’t always log the matched rule line number directly. In this tip, we’ll show how to prove exactly which HBA rule was used, even for different authentication methods, using a small, reusable helper function.

We’ll walk through three real-world authentication patterns:

  • 1. yugabyte – client certificate required

  • 2. appuser – SCRAM password only (no client cert)

  • 3. certuser – client certificate required (non-superuser)

🧱 Environment Setup (YugabyteDB 2025.2)
🔐 Client Certificates

Before starting YugabyteDB, I pre-created all server and client certificates needed for this demo. This mirrors how certificates are typically handled in real environments: created once, stored centrally, and reused consistently across services and users.

Here’s the certificate directory used throughout this post:

				
					[root@localhost ~]# ll /root/yugabyte-certs
total 56
-rw-r--r--. 1 root root 1123 Jan  3 16:38 ca.crt
-rw-------. 1 root root 1704 Jan  3 16:38 ca.key
-rw-r--r--. 1 root root   41 Jan  9 02:19 ca.srl
-rw-r--r--. 1 root root 1155 Jan  9 02:19 certuser-client.crt
-rw-------. 1 root root 1704 Jan  9 02:19 certuser-client.key
-rw-r--r--. 1 root root 1094 Jan  3 16:38 client.crt
-rw-r--r--. 1 root root  887 Jan  3 16:38 client.csr
-rw-------. 1 root root 1704 Jan  3 16:38 client.key
-rw-------. 1 root root 1218 Jan  3 16:38 client.key.pk8
-rw-r--r--. 1 root root 1094 Jan  3 16:38 node.0.0.0.0.crt
-rw-r--r--. 1 root root  891 Jan  3 16:38 node.0.0.0.0.csr
-rw-------. 1 root root 1704 Jan  3 16:38 node.0.0.0.0.key
-rw-r--r--. 1 root root 1155 Jan  9 02:09 yugabyte-client.crt
-rw-------. 1 root root 1704 Jan  9 02:09 yugabyte-client.key
				
			

What these certificates represent:

Certificate Purpose
ca.crt / ca.key Internal Certificate Authority used to sign all server and client certificates
node.*.crt / node.*.key TLS identity used by YugabyteDB TServers
yugabyte-client.crt / yugabyte-client.key Client certificate for the privileged yugabyte user
certuser-client.crt / certuser-client.key Client certificate for an application user that requires cert-based access
client.crt / client.key Generic client certificate (often used for testing or bootstrapping)

Each client certificate was created with a distinct Common Name (CN):

  • CN=yugabyte

  • CN=certuser

With certificates in place, let’s start YugabyteDB and configure HBA rules that mix client certs and SCRAM authentication.

Start yb-master
				
					/root/yugabyte-2025.2.0.0/bin/yb-master \
  --master_addresses=127.0.0.1:7100 \
  --rpc_bind_addresses=127.0.0.1:7100 \
  --fs_data_dirs=/root/var/data \
  --replication_factor=1 \
  &> /root/var/logs/yb-master.out &
				
			
Start yb-tserver (TLS + mixed auth)
				
					/root/yugabyte-2025.2.0.0/bin/yb-tserver \
  --tserver_master_addrs=127.0.0.1:7100 \
  --fs_data_dirs=/root/var/data \
  --certs_for_client_dir=/root/yugabyte-certs \
  --enable_ysql=true \
  --ysql_enable_auth=true \
  --use_client_to_server_encryption=true \
  --ysql_pg_conf_csv="ssl=on,password_encryption=scram-sha-256,log_connections=on" \
  --ysql_hba_conf_csv="\
local all yugabyte ident,\
hostssl all yugabyte 127.0.0.1/32 trust clientcert=verify-ca,\
hostssl all certuser 127.0.0.1/32 trust clientcert=verify-ca,\
hostssl all yugabyte all reject,\
hostnossl all all all reject,\
hostssl all +dbaaccounts all scram-sha-256,\
hostssl all all all scram-sha-256" \
  &> /root/var/logs/yb-tserver.out &

				
			
🧪 Demo Database and Roles

All of the following setup is performed while connected as the yugabyte user, which is the PostgreSQL-compatible superuser in YugabyteDB.

First, connect as yugabyte using the client certificate created earlier:

				
					/root/yugabyte-2025.2.0.0/bin/ysqlsh \
"host=127.0.0.1 port=5433 user=yugabyte sslmode=verify-ca \
sslcert=/root/yugabyte-certs/yugabyte-client.crt \
sslkey=/root/yugabyte-certs/yugabyte-client.key \
sslrootcert=/root/yugabyte-certs/ca.crt"
				
			

Once connected, create the demo database and roles:

				
					-- Demo database used throughout this post
CREATE DATABASE hba_demo_db;

-- Role used to demonstrate role-based HBA matching
CREATE ROLE dbaaccounts;

-- Application user using SCRAM (no client cert required)
CREATE USER appuser WITH PASSWORD 'password123';
GRANT dbaaccounts TO appuser;

-- Application-style user that requires a client certificate
CREATE USER certuser;
				
			

At this point, we have three distinct authentication patterns available:

  • yugabyte – privileged admin user (client cert required)

  • appuser – SCRAM password authentication (no client cert)

  • certuser – client certificate required by HBA

These users will be used in the following sections to demonstrate exactly which pg_hba.conf rule allows each connection, and why.

Oh, since we’re already connected as the yugabyte superuser, let’s take a quick look at the parsed HBA rules as YugabyteDB sees them:

				
					SELECT * FROM pg_hba_file_rules ORDER BY line_number;
				
			

Example:

				
					yugabyte=# SELECT * FROM pg_hba_file_rules ORDER BY line_number;
 line_number |   type    | database |   user_name    |  address  |     netmask     |  auth_method  |        options         | error
-------------+-----------+----------+----------------+-----------+-----------------+---------------+------------------------+-------
           4 | local     | {all}    | {yugabyte}     |           |                 | peer          |                        |
           5 | hostssl   | {all}    | {yugabyte}     | 127.0.0.1 | 255.255.255.255 | trust         | {clientcert=verify-ca} |
           6 | hostssl   | {all}    | {certuser}     | 127.0.0.1 | 255.255.255.255 | trust         | {clientcert=verify-ca} |
           7 | hostssl   | {all}    | {yugabyte}     | all       |                 | reject        |                        |
           8 | hostnossl | {all}    | {all}          | all       |                 | reject        |                        |
           9 | hostssl   | {all}    | {+dbaaccounts} | all       |                 | scram-sha-256 |                        |
          10 | hostssl   | {all}    | {all}          | all       |                 | scram-sha-256 | {clientcert=verify-ca} |
          11 | hostssl   | {all}    | {all}          | all       |                 | md5           |                        |
          12 | local     | {all}    | {yugabyte}     |           |                 | trust         |                        |
(9 rows)
				
			
🛠 Helper Function: util.show_hba_match()

This function reproduces HBA evaluation logic for the current session and returns only the single matched rule.

Key design points:

  • ● Uses SECURITY DEFINER to read pg_hba_file_rules

  • ● Uses session_user (not current_user)

  • ● Safely exposes why the connection succeeded

				
					CREATE SCHEMA IF NOT EXISTS util;

CREATE OR REPLACE FUNCTION util.show_hba_match()
RETURNS TABLE (
  client_ip        text,
  dbname           text,
  username         text,
  application_name text,
  ssl              boolean,
  clientdn         text,
  matched_line     int,
  matched_type     text,
  matched_db       text,
  matched_user     text,
  matched_address  text,
  matched_method   text,
  matched_options  text
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog
AS $$
DECLARE
  v_client_inet inet := inet_client_addr();
  v_client_ip   text := COALESCE(host(inet_client_addr()), 'local');
  v_username    text := session_user;
  v_dbname      text := current_database();
  v_appname     text := COALESCE(current_setting('application_name', true), '');
  v_ssl         boolean := COALESCE(
                           (SELECT s.ssl
                            FROM pg_stat_ssl s
                            WHERE s.pid = pg_backend_pid()),
                           false
                         );
  v_clientdn    text := '';
BEGIN
  -- pg_stat_ssl column name differs by version: client_dn vs clientdn
  IF EXISTS (
    SELECT 1
    FROM pg_attribute
    WHERE attrelid = 'pg_stat_ssl'::regclass
      AND attname = 'client_dn'
  ) THEN
    EXECUTE 'SELECT COALESCE(s.client_dn::text, '''')
             FROM pg_stat_ssl s
             WHERE s.pid = pg_backend_pid()'
      INTO v_clientdn;
  ELSE
    EXECUTE 'SELECT COALESCE(s.clientdn::text, '''')
             FROM pg_stat_ssl s
             WHERE s.pid = pg_backend_pid()'
      INTO v_clientdn;
  END IF;

  RETURN QUERY
  WITH me AS (
    SELECT
      v_client_inet AS client_inet,
      v_client_ip   AS client_ip,
      v_username    AS username,
      v_dbname      AS dbname,
      v_appname     AS application_name,
      v_ssl         AS ssl,
      v_clientdn    AS clientdn
  ),
  candidates AS (
    SELECT r.*
    FROM pg_hba_file_rules r, me
    WHERE
      -- SSL vs non-SSL rule types
      ((me.ssl AND r.type = 'hostssl') OR ((NOT me.ssl) AND r.type = 'hostnossl'))

      -- Database match
      AND ('all' = ANY(r.database) OR me.dbname = ANY(r.database))

      -- User match: all | exact | +role
      AND (
        'all' = ANY(r.user_name)
        OR me.username = ANY(r.user_name)
        OR EXISTS (
          SELECT 1
          FROM unnest(r.user_name) u(entry)
          WHERE left(entry, 1) = '+'
            AND pg_has_role(me.username, substr(entry, 2), 'member')
        )
      )

      -- Address match: 'all' OR client_inet within (address/netmask)
      AND (
        r.address IS NULL
        OR r.address = 'all'
        OR (
          me.client_inet IS NOT NULL
          AND me.client_inet <<= (r.address || '/' || masklen(r.netmask::inet))::cidr
        )
      )
    ORDER BY r.line_number
    LIMIT 1
  )
  SELECT
    me.client_ip,
    me.dbname,
    me.username,
    me.application_name,
    me.ssl,
    NULLIF(me.clientdn, '') AS clientdn,
    c.line_number,
    c.type,
    array_to_string(c.database, ',') AS matched_db,
    array_to_string(c.user_name, ',') AS matched_user,
    CASE
      WHEN c.address IS NULL THEN ''
      WHEN c.address = 'all' THEN 'all'
      ELSE c.address || '/' || masklen(c.netmask::inet)
    END AS matched_address,
    c.auth_method,
    COALESCE(array_to_string(c.options, ','), '') AS matched_options
  FROM me
  LEFT JOIN candidates c ON true;
END;
$$;

GRANT USAGE ON SCHEMA util TO PUBLIC;
GRANT EXECUTE ON FUNCTION util.show_hba_match() TO PUBLIC;
				
			
🔐 Demo 1: yugabyte (Client Certificate Required)
				
					[root@localhost ~]# ysqlsh \
"host=127.0.0.1 port=5433 user=yugabyte sslmode=verify-ca \
sslcert=/root/yugabyte-certs/yugabyte-client.crt \
sslkey=/root/yugabyte-certs/yugabyte-client.key \
sslrootcert=/root/yugabyte-certs/ca.crt" \
-c "SELECT * FROM util.show_hba_match();"
 client_ip |  dbname  | username | application_name | ssl |   clientdn   | matched_line | matched_type | matched_db | matched_user | matched_address | matched_method |   matched_options
-----------+----------+----------+------------------+-----+--------------+--------------+--------------+------------+--------------+-----------------+----------------+----------------------
 127.0.0.1 | yugabyte | yugabyte | ysqlsh           | t   | /CN=yugabyte |            5 | hostssl      | all        | yugabyte     | 127.0.0.1/32    | trust          | clientcert=verify-ca
(1 row)
				
			

Matched HBA line: 5
Auth: trust + clientcert=verify-ca

🔑 Demo 2: appuser (SCRAM, No Cert)
				
					[root@localhost ~]# PGPASSWORD='password123' ysqlsh \
"host=127.0.0.1 port=5433 dbname=hba_demo_db user=appuser" \
-c "SELECT * FROM util.show_hba_match();"
 client_ip |   dbname    | username | application_name | ssl | clientdn | matched_line | matched_type | matched_db | matched_user | matched_address | matched_method | matched_options
-----------+-------------+----------+------------------+-----+----------+--------------+--------------+------------+--------------+-----------------+----------------+-----------------
 127.0.0.1 | hba_demo_db | appuser  | ysqlsh           | t   |          |            9 | hostssl      | all        | +dbaaccounts | all             | scram-sha-256  |
(1 row)
				
			

Matched HBA line: 9
Auth: SCRAM-SHA-256

🔐 Demo 3: certuser (Client Cert Required)
Successful connection
				
					[root@localhost ~]# ysqlsh \
"host=127.0.0.1 port=5433 dbname=hba_demo_db user=certuser sslmode=verify-ca \
sslcert=/root/yugabyte-certs/certuser-client.crt \
sslkey=/root/yugabyte-certs/certuser-client.key \
sslrootcert=/root/yugabyte-certs/ca.crt" \
-c "SELECT * FROM util.show_hba_match();"
 client_ip |   dbname    | username | application_name | ssl |   clientdn   | matched_line | matched_type | matched_db | matched_user | matched_address | matched_method |   matched_options
-----------+-------------+----------+------------------+-----+--------------+--------------+--------------+------------+--------------+-----------------+----------------+----------------------
 127.0.0.1 | hba_demo_db | certuser | ysqlsh           | t   | /CN=certuser |            6 | hostssl      | all        | certuser     | 127.0.0.1/32    | trust          | clientcert=verify-ca
(1 row)
				
			

Matched HBA line: 6
Auth: trust + clientcert=verify-ca

Failure without certificate
				
					[root@localhost ~]# ysqlsh \
"host=127.0.0.1 port=5433 dbname=hba_demo_db user=certuser sslmode=verify-ca \
sslrootcert=/root/yugabyte-certs/ca.crt" \
-c "SELECT 1;"
ysqlsh: error: connection to server at "127.0.0.1", port 5433 failed: FATAL:  connection requires a valid client certificate
				
			
📌 Key Takeaways
  • ● YugabyteDB uses PostgreSQL’s first-match-wins HBA model

  • ● Client certificates are optional and rule-specific

  • ● SCRAM and cert-based auth can coexist on the same port

  • ● Line numbers may shift as rules change — always verify live behavior

  • ● A small helper function can make HBA behavior explainable and provable

🎯 Summary
User Authentication Client Cert HBA Line
yugabyte trust + cert ✅ required 5
certuser trust + cert ✅ required 6
appuser SCRAM ❌ not required 9

Have Fun!

So fun to watch our daughter’s dog, Maple, roll on her back and wrestle her new Christmas toy 😄🐶