When clientcert=verify-ca Doesn’t Do What You Think (YugabyteDB 2024.2 vs 2025.1+)

Client certificates are a common part of securing YugabyteDB clusters, especially when encryption in transit is enabled and pg_hba.conf is used to enforce SSL connections.

Starting in YugabyteDB 2025.1, you’ll see clientcert=verify-ca appear in default HBA entries and documentation. That syntax is correct… for PostgreSQL 15.

However, YugabyteDB 2024.2 is still based on PostgreSQL 11, and this creates a subtle but important trap:

  • On YugabyteDB 2024.2, using clientcert=verify-ca looks like it requires a client certificate… but it does not.

The HBA rule will still match, connections will still succeed, and there are no warnings, but the client certificate requirement is silently ignored.

In this tip, we’ll show:

  • ● why this happens,

  • ● how to prove what YugabyteDB is actually enforcing,

  • ● and how to configure client certificate requirements correctly on both 2024.2 and 2025.1+.

Why this happens: PostgreSQL version matters

YugabyteDB’s YSQL layer is tightly coupled to PostgreSQL, and pg_hba.conf behavior follows the PostgreSQL version underneath it.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

This matters because clientcert syntax changed between PostgreSQL 11 and PostgreSQL 15.

PostgreSQL 11 (YugabyteDB 2024.2)

Supported syntax:

				
					clientcert=1
				
			

clientcert=verify-ca is not supported

PostgreSQL 15 (YugabyteDB 2025.1+)

Supported syntax:

				
					clientcert=verify-ca
clientcert=verify-full
				
			

clientcert=1 is no longer supported

If you use PostgreSQL-15 style syntax on a PostgreSQL-11 based YugabyteDB, the rule may still match, but the client certificate requirement is ignored.

The common pitfall

On YugabyteDB 2024.2, it’s easy to configure something like:

				
					hostssl all all all md5 clientcert=verify-ca
				
			

This looks correct and secure. It is not enforced.

How to prove it (using live introspection)

To prove what YugabyteDB is actually enforcing, we’ll use two tools:

Step 0: Start YugabyteDB 2024.2 processes
				
					# 1. Start yb-master
/root/yugabyte-2024.2.3.1/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 &
  
sleep 5

# 2. Start yb-tserver with strict HBA syntax
/root/yugabyte-2024.2.3.1/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 \
  --use_client_to_server_encryption=true \
  --ysql_pg_conf_csv="ssl=on,password_encryption=md5" \
  --ysql_hba_conf_csv="local all yugabyte ident,hostssl all yugabyte 127.0.0.1/32 trust clientcert=verify-ca,hostssl all yugabyte all reject,hostnossl all all all reject,hostssl all all all md5 clientcert=verify-ca" \
  &> /root/var/logs/yb-tserver.out &

sleep 20
				
			
Step 1: Inspect how PostgreSQL parsed your HBA rules
				
					SELECT * FROM pg_hba_file_rules ORDER BY line_number;
				
			

Example:

				
					[root@localhost ~]# ./yugabyte-2024.2.3.1/bin/ysqlsh -h 127.0.0.1 -p 5433 -U yugabyte -c "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       |         |
           6 | hostssl   | {all}    | {yugabyte} | all       |                 | reject      |         |
           7 | hostnossl | {all}    | {all}      | all       |                 | reject      |         |
           8 | hostssl   | {all}    | {all}      | all       |                 | md5         |         |
           9 | hostssl   | {all}    | {all}      | all       |                 | trust       |         |
          10 | local     | {all}    | {yugabyte} |           |                 | trust       |         |
(7 rows)
				
			

What to look for:

  • ● I did not need a certificate to log in as the yugabyte user

  • ● The options column is NULL / empty

  • PostgreSQL did not recognize the clientcert=verify-ca option

This is your first indication that the requirement is being ignored.

Step 2: Show which rule actually allowed your connection

 Now run:

				
					SELECT * FROM util.show_hba_match();
				
			

Example:

				
					[root@localhost ~]# ./yugabyte-2024.2.3.1/bin/ysqlsh -h 127.0.0.1 -p 5433 -U yugabyte -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   |          |            5 | hostssl      | all        | yugabyte     | 127.0.0.1/32    | trust          |
(1 row)
				
			

There is also another user in the system named appuser. Let’s check which HBA rule it matches:

				
					[root@localhost ~]# PGPASSWORD=password123 ./yugabyte-2024.2.3.1/bin/ysqlsh -h 127.0.0.1 -p 5433 -U 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 | yugabyte | appuser  | ysqlsh           | t   |          |            8 | hostssl      | all        | all          | all             | md5            |
(1 row)
				
			

Above we see:

  • ● A hostssl ... md5 and hostssl ... trust rule matched

  • matched_options is empty

  • ● The connections succeeded without a client certificate

💥 This confirms that no client certificate was required, despite what the HBA line appears to say.

You may notice that a hostssl rule matched even though SSL was not explicitly requested. This is expected behavior… most PostgreSQL clients (i.e. psql, ysqlsh)  attempt to use SSL by default if the server supports it, so the connection is automatically upgraded to SSL and evaluated against hostssl rules.

The correct way to require client certificates on 2024.2

If you want client certificates to be enforced on YugabyteDB 2024.2, you must use PostgreSQL-11 syntax:

				
					clientcert=1
				
			

Example:

				
					hostssl all all all md5 clientcert=1
				
			
Step 0: Restart the YB-TSERVER process
				
					pkill -9 yb-tserver; pkill -9 postgres;

/root/yugabyte-2024.2.3.1/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 \
  --use_client_to_server_encryption=true \
  --ysql_pg_conf_csv="ssl=on,password_encryption=md5" \
  --ysql_hba_conf_csv="local all yugabyte ident,hostssl all yugabyte 127.0.0.1/32 trust clientcert=1,hostssl all yugabyte all reject,hostnossl all all all reject,hostssl all all all md5 clientcert=1" \
  &> /root/var/logs/yb-tserver.out &
				
			
Step 1: Inspect how PostgreSQL parsed your HBA rules
				
					[root@localhost ~]# ./yugabyte-2024.2.3.1/bin/ysqlsh -h 127.0.0.1 -p 5433 -U yugabyte -c "SELECT * FROM pg_hba_file_rules ORDER BY line_number;"
ysqlsh: FATAL:  connection requires a valid client certificate
FATAL:  pg_hba.conf rejects connection for host "127.0.0.1", user "yugabyte", database "yugabyte", SSL off
				
			

Right away we see now that a client certificate is needed to log in as the yugabyte user.

				
					[root@localhost ~]# /root/yugabyte-2024.2.3.1/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" -c "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=true} |
           6 | hostssl   | {all}    | {yugabyte} | all       |                 | reject      |                   |
           7 | hostnossl | {all}    | {all}      | all       |                 | reject      |                   |
           8 | hostssl   | {all}    | {all}      | all       |                 | md5         | {clientcert=true} |
           9 | hostssl   | {all}    | {all}      | all       |                 | trust       |                   |
          10 | local     | {all}    | {yugabyte} |           |                 | trust       |                   |
(7 rows)
				
			

Also, pg_hba_file_rules.options now shows:

				
					{clientcert=true}
				
			
Step 2: Show which rule actually allowed your connection
				
					[root@localhost ~]# /root/yugabyte-2024.2.3.1/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" -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=true
(1 row)

[root@localhost ~]# PGPASSWORD=password123 /root/yugabyte-2024.2.3.1/bin/ysqlsh "host=127.0.0.1 port=5433 user=appuser" -c "SELECT * FROM util.show_hba_match();"
ysqlsh: FATAL:  connection requires a valid client certificate
FATAL:  pg_hba.conf rejects connection for host "127.0.0.1", user "appuser", database "yugabyte", SSL off

[root@localhost ~]# PGPASSWORD=password123 /root/yugabyte-2024.2.3.1/bin/ysqlsh "host=127.0.0.1 port=5433 user=appuser sslmode=verify-ca \
sslcert=/root/yugabyte-certs/client.crt \
sslkey=/root/yugabyte-certs/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 | appuser  | ysqlsh           | t   | /CN=appuser |            8 | hostssl      | all        | all          | all             | md5            | clientcert=true
(1 row)
				
			

This is the difference between:

  • ● appearing secure, and

  • ● actually enforcing client certificates.

What is the clientcert option really for?

This is an important distinction that often gets blurred.

clientcert=verify-ca

  • ● Requires the client to present a certificate

  • ● Verifies it was signed by a trusted CA

  • ● Does not authenticate the database user

Think of this as proof of membership, not identity:

  • “You have a certificate issued by our CA.”

clientcert=verify-full (PostgreSQL 15+)

  • ● Verifies CA signature

  • ● Verifies the certificate Common Name (CN) matches the database user (or mapping rules)

  • ● Much closer to identity-backed authentication

For more background, see Sanketh Indarapu‘s blog post YugabyteDB’s Latest Encryption in Transit Developments for YSQL!

Why this works correctly in YugabyteDB 2025.1+

Starting in YugabyteDB 2025.1, YSQL is based on PostgreSQL 15, which:

  • ● Fully supports clientcert=verify-ca

  • ● Fully supports clientcert=verify-full

  • ● Removes support for clientcert=1

Practical guidance
If you are on YugabyteDB 2024.2
  • ● ✅ Use clientcert=1

  • ● ❌ Do not rely on clientcert=verify-ca

  • ● Always validate using:

    • pg_hba_file_rules

    • util.show_hba_match()

If you are on YugabyteDB 2025.1+
  • ● ✅ Use clientcert=verify-ca or verify-full

  • ● ❌ Do not use clientcert=1

Final takeaway

Always verify what PostgreSQL is enforcing… not just what your HBA file says.

pg_hba_file_rules and util.show_hba_match() should be part of every serious YugabyteDB security review. 💪

Have Fun!

While driving home from dinner last night, I spotted an electronic billboard at a local shopping center flashing an unexpected error: “The server that the application is connecting to is unverified!” 😂 Looks like an expired cert... broadcast loud and clear for everyone to see!