Skip to content

feat: caching_sha2_password: RSA public-key support for plaintext clients, and Admin credential usability #5985

Description

@ademidoff

Summary

With mysql-default_authentication_plugin=caching_sha2_password, ProxySQL sends the
perform full authentication marker but implements neither of the two ways the MySQL
protocol allows a client to answer it:

  1. No RSA public-key exchange. When a client on a plaintext connection sends the
    request_public_key packet (0x02), ProxySQL replies with something that is not
    AuthMoreData, so the client aborts. MySQL server answers this request
    (caching_sha2_password_auto_generate_rsa_keys=ON by default), which is why the same clients
    work against MySQL without TLS.
  2. Cleartext-over-TLS is rejected on the Admin interface. ProxySQL does force CLIENT_SSL
    on for this plugin, so TLS is available and negotiates fine — but when the client then sends
    the cleartext password that full auth requires, the Admin module answers Access denied for
    credentials whose stored form is a caching_sha2_password hash, and for the
    mysql-monitor_* credential.

Net effect: for the affected credentials there is no working path at all — plaintext fails
with perform full authentication, TLS fails with Access denied.

Client-side error text (go-sql-driver/mysql v1.10.0, but any client hits the same wire
behaviour):

unexpected resp from server for caching_sha2_password, perform full authentication

Environment

  • ProxySQL 2.7.0 (proxysql/proxysql:2.7.0) and 3.0.10 (proxysql/proxysql:3.0.10,
    admin-version = 3.0.10-426-gf5f1e14) — every result below reproduces identically on both,
    including the CLIENT_SSL/have_ssl behaviour and all six rows of the Finding 3 matrix
  • mysql-have_ssl=false, matching how ProxySQL ships in the Percona XtraDB Cluster Operator
  • Client: github.com/go-sql-driver/mysql v1.10.0; cross-checked with the MariaDB mysql CLI in
    the image

Finding 1 — Admin interface: a mysql_users row silently breaks a valid Admin credential

This one is fully deterministic. monitor is a valid admin-admin_credentials entry stored in
cleartext; adding a mysql_users row of the same name holding a caching_sha2 hash flips the
Admin login onto the full-auth path, which can then never complete:

SET mysql-default_authentication_plugin='caching_sha2_password';
SET mysql-have_ssl='false';
LOAD MYSQL VARIABLES TO RUNTIME;
SET admin-admin_credentials='admin:admin;monitor:monitorpass';
LOAD ADMIN VARIABLES TO RUNTIME;
INSERT INTO mysql_users(username,password,default_hostgroup,frontend,backend)
  VALUES('monitor', CACHING_SHA2_PASSWORD('monitorpass'),0,1,1);
LOAD MYSQL USERS TO RUNTIME;

Connecting to :6032 as monitor/monitorpass:

connection result
plaintext unexpected resp from server for caching_sha2_password, perform full authentication
tls=skip-verify ERROR 1045 (28000): ProxySQL Error: Access denied for user 'monitor'@'127.0.0.1'

Remove the mysql_users row and both succeed. Switch the plugin to mysql_native_password and
both succeed with the row in place. Note a mysql_users credential is never valid for the Admin
interface on its own (it returns plain Access denied) — yet its mere presence changes the Admin
handshake for a same-named Admin credential.

Finding 2 — the mysql-monitor_* credential stops working on the Admin interface

mysql-monitor_username/mysql-monitor_password authenticates on :6032 under
mysql_native_password and is rejected under caching_sha2_password, with the password stored
in cleartext exactly as documented:

default plugin monitor:6032, plaintext
mysql_native_password OK
caching_sha2_password Access denied

This is what #5363 reports (k8s liveness/readiness probes failing with
Access denied for user 'monitor'@'127.0.0.1', open since February with no reply) and what the
follow-up comment on #4845 asks about. #4845 has the maintainer statement that
mysql-monitor_password must be cleartext — it is cleartext here, and it still fails.

Finding 3 — full matrix, Admin interface :6032

All rows with caching_sha2_password and mysql-have_ssl=false:

credential plaintext TLS
in admin-admin_credentials, no same-named mysql_users row OK OK
in admin-admin_credentials, shadowed by a hashed mysql_users row full-auth error Access denied
mysql_users only, caching_sha2 hash full-auth error Access denied
mysql_users only, cleartext password Access denied Access denied
dedicated name in admin-stats_credentials OK OK
(mysql_native_password) shadowed Admin credential OK OK

The frontend (:6033) also produces the full-auth error for these credentials, but in our
testing the outcome there varied between otherwise-identical instances, so we're only asserting
the Admin results.

Reproducer: a self-contained script that spins up a fresh container per assertion (ProxySQL
auth state turned out to be order-dependent, so shared instances give inconsistent results) and
asserts every row above — 13 checks, all passing on both 2.7.0 and 3.0.10:
(attach or link the script)

Root cause in source

CLIENT_SSL is forced on whenever the default plugin is caching_sha2_password, independent of
have_ssllib/MySQL_Protocol.cpp:1036-1043 at tag v2.7.0, unchanged at
lib/MySQL_Protocol.cpp:1083-1090 at tag v3.0.10 and on the v3.0 branch:

if (mysql_thread___have_ssl==true || mysql_thread___default_authentication_plugin_int==2) {
    // we enable SSL for client connections for either of these 2 conditions:
    // - have_ssl is enabled
    // - default_authentication_plugin=caching_sha2_password
    mysql_thread___server_capabilities |= CLIENT_SSL;
} else {
    mysql_thread___server_capabilities &= ~CLIENT_SSL;
}

==2 is caching_sha2_password (lib/MySQL_Thread.cpp:1854-1863 at v2.7.0,
lib/MySQL_Thread.cpp:2366 at v3.0.10). The Admin interface emits its greeting through this
same function (lib/ProxySQL_Admin.cpp:5590 at v2.7.0, lib/ProxySQL_Admin.cpp:2235 at
v3.0.10), which is why :6032 behaves like :6033 here. There is no code path that serves a server RSA public key or decrypts an
RSA-encrypted password.

To be explicit: forcing CLIENT_SSL is the right call and we are not asking for it to be
reverted
— it is what makes a completion path possible at all. The gap is that neither
completion path actually works.

How MySQL differs (8.0.46, verified with the same client)

MySQL 8.0.46 greeting caching_sha2 user, plaintext connection
TLS on (default) CLIENT_SSL=true OK — RSA full auth, cache cleared beforehand
TLS off (--tls-version=, have_ssl=DISABLED) CLIENT_SSL=false OK — no TLS involved at any point

MySQL also implements the cache the plugin is named for, so full auth only happens on first
connect, after FLUSH PRIVILEGES, or after a password change.

Worth pre-empting one thing: the ERROR 2061 ... Authentication requires secure connection
people quote from the mysql CLI is not server-side TLS enforcement — it is the client
declining to fetch a public key it wasn't told to trust:

mysql --ssl-mode=DISABLED                          -> ERROR 2061 ... requires secure connection
mysql --ssl-mode=DISABLED --get-server-public-key  -> connected

Asks, in order of preference

  1. Serve the RSA public key and accept an RSA-encrypted password, matching MySQL's
    --get-server-public-key / --server-public-key-path flow. This fixes non-TLS clients
    generally.
  2. Fix the Admin module's full-auth completion so a cleartext password received over TLS is
    verified against the stored credential — including admin-*_credentials and
    mysql-monitor_*. This alone would make TLS a genuine workaround, which it currently isn't.
  3. Stop a mysql_users row from shadowing a same-named Admin credential during the Admin
    handshake (Finding 1).
  4. Failing all of the above: document the limitation — which credential classes can
    authenticate to the Admin interface under caching_sha2_password, that hashed mysql_users
    entries cannot, and that TLS does not help — and return an error naming the real cause instead
    of an unexpected packet.

Impact

caching_sha2_password is the MySQL 8.0+ default and mysql_native_password was removed in
MySQL 9.0, so users are increasingly provisioned with caching_sha2 credentials. Today the only
reliable workarounds are to keep ProxySQL's frontend on the deprecated mysql_native_password,
or to give each admin-side client a dedicated admin-*_credentials name that appears nowhere in
mysql_users. Monitoring is hit first — health probes (#5363) and metrics exporters connecting
to :6032 — but any client of the Admin interface is affected.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions