fix(passthrough-auth): gate cache eviction on credential-was-from-cache

The ER_ACCESS_DENIED_ERROR (1045) eviction hook in
handler_again___status_CONNECTING_SERVER fired unconditionally on any
1045 for the session's username. A regular mysql_users user with a
stale stored hash that triggered 1045 would silently evict an
unrelated pass-through cache entry for the same username.

Functionally this is harmless (next pass-through connect re-probes
and re-populates the cache), but it creates needless churn on
rotation events for users who aren't using pass-through at all,
and the metric cache_invalidations becomes noisy.

Add a per-session @c passthrough_credential bool on MySQL_Session:
  - Initialized to @c false in MySQL_Session::reset() alongside the
    other auth-state flags.
  - Set to @c true in PPHR_verify_password on a cache hit (the
    credential we're verifying with literally came from the cache).
  - Set to @c true in
    handler_again___status_AUTHENTICATING_BACKEND_FOR_CLIENT after
    a probe success (we just inserted the cleartext into the cache,
    so a future ER 1045 on this session means our just-cached
    cleartext is stale).

The eviction hook now checks this flag before evicting; non-
pass-through sessions cannot invalidate the cache. This makes
cache_invalidations a clean signal for "pass-through sessions
whose cached cleartext was rejected by the backend".

Discovered as N4 / a GA-blocker by the production-readiness
subagent during the round-3 deep review of PR #5810.
docs/passthrough-auth-spec
Rene Cannao 1 month ago
parent d907797944
commit a050d0d431

@ -526,6 +526,31 @@ class MySQL_Session: public Base_Session<MySQL_Session, MySQL_Data_Stream, MySQL
// this is used ONLY for Admin, and only if the other party is another proxysql instance part of a cluster
bool use_ldap_auth;
/**
* @brief Set to @c true when this session's credential came from the
* pass-through cache or a fresh pass-through probe (spec §8.4).
*
* Read by the @c ER_ACCESS_DENIED_ERROR eviction hook in
* @c handler_again___status_CONNECTING_SERVER: only sessions whose
* credential was supplied by the pass-through machinery are allowed
* to invalidate the cache entry on a backend 1045. Without this
* flag, a regular @c mysql_users user with the same name but a
* stale stored hash would evict an unrelated pass-through cache
* entry -- needless churn for users who aren't even using
* pass-through.
*
* Set in two places:
* - @c PPHR_verify_password on a cache hit
* (the cached cleartext IS what we're authenticating with).
* - @c handler_again___status_AUTHENTICATING_BACKEND_FOR_CLIENT
* on probe success (we just inserted the cleartext into the
* cache and immediately put it on the session's userinfo).
*
* Cleared on session reset / re-init alongside the other auth
* state. Stays @c false for regular @c mysql_users authentications.
*/
bool passthrough_credential;
// Fast forward grace close flags: track backend closure during fast forward mode
// to allow pending client data to drain before closing the session.
bool backend_closed_in_fast_forward;

@ -2682,6 +2682,16 @@ bool MySQL_Protocol::PPHR_verify_password(MyProt_tmp_auth_vars& vars1, account_d
if (GloMyPTAuthCache->lookup(
std::string((const char*)vars1.user), cleartext, ttl_s)) {
GloMyPTAuthCache->bump_cache_hits();
/*
* Mark the session: the credential being used to verify
* this client connection came from the pass-through
* cache, so the backend-rejection eviction hook in
* handler_again___status_CONNECTING_SERVER is permitted
* to invalidate the entry on a future ER_ACCESS_DENIED.
*/
if ((*myds) && (*myds)->sess) {
(*myds)->sess->passthrough_credential = true;
}
if (vars1.password) { free(vars1.password); }
vars1.password = strdup(cleartext.c_str());
/**

@ -710,6 +710,7 @@ MySQL_Session::MySQL_Session() {
last_HG_affected_rows = -1; // #1421 : advanced support for LAST_INSERT_ID()
proxysql_node_address = NULL;
use_ldap_auth = false;
passthrough_credential = false;
this->wait_timeout = mysql_thread___wait_timeout;
backend_closed_in_fast_forward = false;
fast_forward_grace_start_time = 0;
@ -1976,6 +1977,15 @@ int MySQL_Session::handler_again___status_AUTHENTICATING_BACKEND_FOR_CLIENT() {
// Probe succeeded — cache the learned credential.
GloMyPTAuthCache->insert(std::string(username), std::string(cleartext), target_hg);
/*
* Mark the session: the cleartext we're about to assign onto
* userinfo->password came from the probe (i.e. is now in the
* pass-through cache). Authorize the backend-rejection eviction
* hook to invalidate it on a future ER_ACCESS_DENIED for this
* session, distinct from same-username sessions that authenticated
* via a regular mysql_users row.
*/
passthrough_credential = true;
// Update userinfo with the learned cleartext. process_pkt_handshake_response
// stored "" for userinfo->password since PPHR_verify_password returned
@ -3467,13 +3477,31 @@ bool MySQL_Session::handler_again___status_CONNECTING_SERVER(int *_rc) {
goto __exit_handler_again___status_CONNECTING_SERVER_with_err;
break;
case ER_ACCESS_DENIED_ERROR: // 1045
// Pass-through cache invalidation (spec §8.4).
// If a backend rejected a connection using a
// password that came from the pass-through cache,
// the cached cleartext is stale. Evict it so the
// next connect from this user re-probes.
/*
* Pass-through cache invalidation (spec §8.4).
*
* Gated on @c sess->passthrough_credential so
* the eviction fires only for sessions whose
* credential actually came from the cache (or a
* fresh probe that just populated the cache).
*
* Without the gate, a backend 1045 against a
* regular @c mysql_users user with the SAME
* username as a separately-cached pass-through
* entry would evict that unrelated entry --
* harmless functionally (next connect re-probes),
* but causes needless churn on rotation events
* for users who aren't even using pass-through.
*
* The flag is set by:
* - PPHR_verify_password on cache hit
* - the AUTHENTICATING_BACKEND_FOR_CLIENT
* handler on probe success
* and stays @c false for normal authentications.
*/
if (GloMyPTAuthCache != NULL
&& mysql_thread___passthrough_auth_enabled
&& passthrough_credential
&& client_myds && client_myds->myconn
&& client_myds->myconn->userinfo
&& client_myds->myconn->userinfo->username) {

Loading…
Cancel
Save