Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd642d9
feat(auth): add caching_sha2_password RSA key exchange
renecannao Aug 9, 2026
f5f4794
fix: persist rejected caching SHA-2 RSA config
renecannao Aug 9, 2026
c7ae9cb
fix: avoid double counting rejected RSA config
renecannao Aug 9, 2026
a8f0114
fix: harden caching SHA-2 RSA buffers
renecannao Aug 9, 2026
346cf3b
fix: retain RSA plaintext allocation for cleansing
renecannao Aug 9, 2026
958dff5
fix: harden auth packet construction
renecannao Aug 9, 2026
c2a05a5
chore: untrack task 3 report
renecannao Aug 9, 2026
875fa65
fix: remediate caching SHA-2 RSA quality findings
renecannao Aug 9, 2026
3f8bac9
fix: initialize protocol auth failure fields
renecannao Aug 9, 2026
1a8ff95
docs: clarify RSA auth helper contracts
renecannao Aug 9, 2026
c38d04e
test: escape wildcard TAP descriptions
renecannao Aug 9, 2026
bb74515
test: align RSA rejection E2E with Admin refresh
renecannao Aug 9, 2026
27b853d
fix: wait for in-flight RSA key publication
renecannao Aug 9, 2026
9a3fe88
fix: serialize admin MySQL variable commits
renecannao Aug 9, 2026
d484a5e
test: make RSA publication race deterministic
renecannao Aug 9, 2026
0d1c9f5
fix: address Sonar RSA review findings
renecannao Aug 10, 2026
855abce
Merge remote-tracking branch 'origin/v3.0' into feature/caching-sha2-…
renecannao Aug 10, 2026
ba03b99
test: gate caching SHA-2 RSA expectations by version
renecannao Aug 10, 2026
8870c4f
test: address final CodeRabbit findings
renecannao Aug 10, 2026
2fdc294
Merge remote-tracking branch 'origin/v3.0' into feature/caching-sha2-…
renecannao Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions doc/caching_sha2_password_rsa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# RSA key exchange for `caching_sha2_password`

ProxySQL 3.1 can authenticate MySQL clients that use
`caching_sha2_password` over a non-TLS frontend connection. When full
authentication is required, the client can request ProxySQL's RSA public key,
encrypt its password, and send the ciphertext back to ProxySQL.

TLS remains the recommended configuration. Requesting a public key over an
unauthenticated connection encrypts the password on the wire, but it does not
authenticate the ProxySQL server and is vulnerable to public-key substitution
by an active network attacker. Use TLS when server identity and transport
integrity are required.

## Configuration

The following MySQL variables are available in ProxySQL 3.1 and later:

| Variable | Default | Description |
| --- | --- | --- |
| `mysql-caching_sha2_password_auto_generate_rsa_keys` | `true` | Generate a 2048-bit RSA pair when both configured files are absent. |
| `mysql-caching_sha2_password_private_key_path` | `proxysql-caching-sha2-private-key.pem` | Private-key path. A relative path is resolved below ProxySQL's data directory. |
| `mysql-caching_sha2_password_public_key_path` | `proxysql-caching-sha2-public-key.pem` | Public-key path. A relative path is resolved below ProxySQL's data directory. |

Apply changes with:

```sql
LOAD MYSQL VARIABLES TO RUNTIME;
```

The three variables form one configuration unit. ProxySQL validates or
generates the complete pair before publishing it to frontend sessions. If a
reload fails, all three runtime values and the previously loaded key snapshot
remain unchanged.

Relative paths must stay beneath ProxySQL's data directory. Empty, `.` and
`..` components are rejected, and every parent directory is opened without
following symbolic links. Absolute paths are allowed when keys are managed in
another operator-controlled directory.

## Key formats and permissions

The private key must be an unencrypted PKCS#8 PEM RSA private key (the PEM
header is `BEGIN PRIVATE KEY`). Traditional PKCS#1 (`BEGIN RSA PRIVATE KEY`)
and encrypted private keys are rejected. The public key must be a PEM
SubjectPublicKeyInfo public key. The two files must contain a structurally
valid matching RSA pair of at least 2048 bits.

The private file must be a regular file and must not grant any group or other
permissions. Generated files use these modes:

- private key: `0600`
- public key: `0644`

Encrypted private keys are not supported because ProxySQL has no runtime
passphrase input for this feature.

If the compiled default pair is unusable during initial runtime loading and
cannot be regenerated safely, ProxySQL records an explicit TLS-only state
(automatic generation off and both paths empty). TLS authentication remains
available, while RSA public-key authentication stays disabled until a valid
pair is loaded.

Automatic generation occurs only when both paths are absent. If exactly one
file exists, ProxySQL reports a configuration error and does not overwrite or
replace either path. Generation uses temporary files and no-overwrite
publication so concurrent ProxySQL processes cannot publish a mixed pair.

## Reload and cluster behavior

Each authentication exchange retains the same immutable key snapshot from the
public-key response through RSA decryption. A concurrent
`LOAD MYSQL VARIABLES TO RUNTIME` can therefore rotate keys without breaking
an exchange already in progress.

Cluster synchronization transfers the variable values, not private-key
contents. Every ProxySQL node must be able to read its configured local pair,
or generate its own pair when automatic generation is enabled. Do not store
private-key contents in the ProxySQL configuration database.

## Client behavior and failures

The client must use `caching_sha2_password`, disable TLS only when intended,
and enable its server-public-key request option. For Oracle's MySQL CLI:

```bash
mysql --default-auth=caching_sha2_password \
--ssl-mode=DISABLED --get-server-public-key \
--host=127.0.0.1 --port=6033 --user=app --password
```

ProxySQL implements the MySQL protocol's RSA OAEP exchange, including the
protocol-defined SHA-1 OAEP and MGF1 digests and password/scramble XOR step.
Malformed ciphertext, malformed plaintext, and an incorrect password all
produce the normal `1045` / `28000` access-denied response. If no valid RSA key
pair is available, the same error code and SQLSTATE are returned with a message
that identifies the unavailable RSA key exchange and suggests TLS or key
configuration.
9 changes: 8 additions & 1 deletion doc/internal/passthrough_authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,14 @@ Entry includes username, source IP, hostgroup probed, outcome. Useful for forens

### 7.5 RSA public key for non-TLS clients

MySQL's `caching_sha2_password` allows non-TLS clients to encrypt the cleartext password with the server's RSA public key. If we want to support non-TLS pass-through, ProxySQL needs to publish a public key (`caching_sha2_password_public_key_path`) and decrypt with the matching private key. Phase 1 ships without this; clients must use TLS. Phase 2 may add RSA support if there's demand.
ProxySQL 3.1 adds the frontend RSA public-key exchange for
`caching_sha2_password`; see
[`doc/caching_sha2_password_rsa.md`](../caching_sha2_password_rsa.md). This lets
frontend users complete full authentication without TLS. Pass-through keeps
its secure default (`mysql-passthrough_auth_require_tls=true`). If an operator
explicitly disables that gate, the same RSA exchange can supply the cleartext
credential used by the backend authentication probe; the public-key
substitution warning in the linked document applies.

## 8. The cache

Expand Down
57 changes: 57 additions & 0 deletions include/MySQL_Caching_Sha2_RSA.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#ifndef PROXYSQL_MYSQL_CACHING_SHA2_RSA_H
#define PROXYSQL_MYSQL_CACHING_SHA2_RSA_H
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

#include <memory>
#include <mutex>
#include <string>

#include <openssl/types.h>

struct CachingSha2RSAConfig {
bool auto_generate { true };
std::string private_key_path;
std::string public_key_path;
std::string datadir;
};

class CachingSha2RSAKeySnapshot {
public:
const std::string& public_key_pem() const { return public_key_pem_; }
size_t ciphertext_size() const { return ciphertext_size_; }

private:
friend class MySQL_Caching_Sha2_RSA;
std::shared_ptr<EVP_PKEY> private_key_;
std::string public_key_pem_;
std::string private_key_path_;
std::string public_key_path_;
size_t ciphertext_size_ { 0 };
};

struct CachingSha2RSAReloadResult {
bool accepted { false };
bool changed { false };
bool available { false };
std::string error;
};

class MySQL_Caching_Sha2_RSA {
public:
CachingSha2RSAReloadResult reload(const CachingSha2RSAConfig& config);
std::shared_ptr<const CachingSha2RSAKeySnapshot> acquire() const;
bool decrypt_password(
const std::shared_ptr<const CachingSha2RSAKeySnapshot>& snapshot,
const unsigned char* ciphertext,
size_t ciphertext_length,
const unsigned char* scramble,
size_t scramble_length,
std::string& password,
std::string* error = nullptr
) const;

private:
mutable std::mutex mutex_;
std::shared_ptr<const CachingSha2RSAKeySnapshot> snapshot_;
};

#endif
7 changes: 4 additions & 3 deletions include/MySQL_Passthrough_Auth_Cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@

class MySQL_Passthrough_Auth_Cache {
private:
struct entry_t {

Check failure on line 38 in include/MySQL_Passthrough_Auth_Cache.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Customize this struct's copy constructor to participate in resource management. Customize or delete its copy assignment operator. Also consider whether move operations should be customized.

See more on https://sonarcloud.io/project/issues?id=sysown_proxysql&issues=AZ_niK95MZUky4BdUyP1&open=AZ_niK95MZUky4BdUyP1&pullRequest=6017
std::string cleartext_password;
uint64_t learned_at_us;
int hostgroup_probed;
uint64_t learned_at_us { 0 };
int hostgroup_probed { 0 };
~entry_t();
};
mutable pthread_rwlock_t lock;
std::unordered_map<std::string, entry_t> entries;
Expand Down Expand Up @@ -109,7 +110,7 @@
bool lookup(const std::string& username, std::string& out_cleartext, uint32_t ttl_s);

// Insert or replace a cached credential.
void insert(const std::string& username, const std::string& cleartext, int hostgroup_probed);
void insert(const std::string& username, const char* cleartext, int hostgroup_probed);

// Evict a single entry. Returns true if the entry was present.
bool evict(const std::string& username);
Expand Down
22 changes: 22 additions & 0 deletions include/MySQL_Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
#include "MySQL_Variables.h"
#include "MySQL_Prepared_Stmt_info.h"

#ifdef PROXYSQL31
#include <memory>

class CachingSha2RSAKeySnapshot;

enum class MySQLFrontendAuthError : uint8_t {
NONE = 0,
CACHING_SHA2_RSA_UNAVAILABLE
};
#endif

#define RESULTSET_BUFLEN 16300

extern MySQL_Variables mysql_variables;
Expand Down Expand Up @@ -112,6 +123,9 @@ class MyProt_tmp_auth_vars {
uint8_t zstd_compression_level = 0;
bool use_ssl = false;
bool use_zstd_compression = false;
#ifdef PROXYSQL31
bool pass_is_sensitive = false;
#endif
enum proxysql_session_type session_type;
};

Expand Down Expand Up @@ -141,6 +155,10 @@ class MySQL_Protocol {
enum proxysql_auth_plugins auth_plugin_id;
uint16_t prot_status;
bool more_data_needed;
#ifdef PROXYSQL31
std::shared_ptr<const CachingSha2RSAKeySnapshot> caching_sha2_rsa_snapshot_;
MySQLFrontendAuthError frontend_auth_error_ { MySQLFrontendAuthError::NONE };
#endif
MySQL_Data_Stream *get_myds() { return *myds; }
MySQL_Protocol()
: userinfo(nullptr), sess(nullptr), myds(nullptr), current_PreStmt(nullptr)
Expand Down Expand Up @@ -214,6 +232,10 @@ class MySQL_Protocol {
bool PPHR_verify_password_2(MyProt_tmp_auth_vars& vars1, account_details_t& account_details);

void generate_one_byte_pkt(unsigned char b);
#ifdef PROXYSQL31
void generate_auth_more_data(const unsigned char *data, size_t data_len);
MySQLFrontendAuthError consume_frontend_auth_error();
#endif

bool process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned int len);
void * Query_String_to_packet(uint8_t sid, std::string *s, unsigned int *l);
Expand Down
27 changes: 26 additions & 1 deletion include/MySQL_Thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <sys/epoll.h>
#endif // IDLE_THREADS
#include <atomic>
#include <memory>
#include <string>

#include "prometheus_helpers.h"

Expand All @@ -38,6 +40,14 @@

extern class MySQL_Variables mysql_variables;

#ifdef PROXYSQL31
class MySQL_Caching_Sha2_RSA;
#endif

struct MySQLThreadsCommitResult {
unsigned int rejected_variables { 0 };
};

#ifdef IDLE_THREADS
typedef struct __attribute__((aligned(64))) _conn_exchange_t {
pthread_mutex_t mutex_idles;
Expand Down Expand Up @@ -427,6 +437,13 @@ class MySQL_Threads_Handler
// variable address
// special variable : if true, further input validation is required
std::unordered_map<std::string, std::tuple<bool *, bool>> VariablesPointers_bool;
#ifdef PROXYSQL31
std::unique_ptr<MySQL_Caching_Sha2_RSA> caching_sha2_rsa_manager_;
bool caching_sha2_rsa_config_initialized_ { false };
bool caching_sha2_rsa_accepted_auto_generate_ { true };
std::string caching_sha2_rsa_accepted_private_path_;
std::string caching_sha2_rsa_accepted_public_path_;
#endif
/**
* @brief Holds the clients host cache. It keeps track of the number of
* errors associated to a specific client:
Expand Down Expand Up @@ -521,6 +538,11 @@ class MySQL_Threads_Handler
int select_version_forwarding;
char *keep_multiplexing_variables;
char *default_authentication_plugin;
#ifdef PROXYSQL31
bool caching_sha2_password_auto_generate_rsa_keys;
char *caching_sha2_password_private_key_path;
char *caching_sha2_password_public_key_path;
#endif
char *proxy_protocol_networks;
//unsigned int default_charset; // removed in 2.0.13 . Obsoleted previously using MySQL_Variables instead
int handle_unknown_charset;
Expand Down Expand Up @@ -790,11 +812,14 @@ class MySQL_Threads_Handler
unsigned int get_global_version();
void wrlock();
void wrunlock();
void commit();
MySQLThreadsCommitResult commit();
char *get_variable(char *name);
bool set_variable(char *name, const char *value);
char **get_variables_list();
bool has_variable(const char * name);
#ifdef PROXYSQL31
MySQL_Caching_Sha2_RSA* caching_sha2_rsa() const { return caching_sha2_rsa_manager_.get(); }
#endif

MySQL_Threads_Handler();
~MySQL_Threads_Handler();
Expand Down
1 change: 1 addition & 0 deletions include/mysql_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class MySQL_Connection_userinfo {
char *fe_username;
MySQL_Connection_userinfo();
~MySQL_Connection_userinfo();
void clear_password();
void set(char *, char *, char *, char *);
void set(MySQL_Connection_userinfo *);
bool set_schemaname(char *, int);
Expand Down
6 changes: 5 additions & 1 deletion lib/Admin_FlushVariables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,11 @@ FlushVariableStats ProxySQL_Admin::flush_mysql_variables___database_to_runtime(S
free(default_collation_connection);
free(previous_default_charset);
free(previous_default_collation_connection);
GloMTH->commit();
const MySQLThreadsCommitResult commit_result = GloMTH->commit();
if (commit_result.rejected_variables != 0) {
stats.updated = std::max(0, stats.updated - static_cast<int>(commit_result.rejected_variables));
stats.rejected += static_cast<int>(commit_result.rejected_variables);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
GloMTH->wrunlock();

{
Expand Down
4 changes: 4 additions & 0 deletions lib/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo
Query_Processor_ParserSQL.oo \
proxy_sqlite3_symbols.oo

ifeq ($(PROXYSQL31),1)
_OBJ_CXX += MySQL_Caching_Sha2_RSA.oo
endif

# TSDB object files
ifeq ($(PROXYSQLTSDB),1)
_OBJ_CXX += TSDB_Dashboard_html.oo
Expand Down
Loading
Loading