Skip to content

Commit cd642d9

Browse files
committed
feat(auth): add caching_sha2_password RSA key exchange
1 parent 2bd3c17 commit cd642d9

23 files changed

Lines changed: 2697 additions & 159 deletions

doc/caching_sha2_password_rsa.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# RSA key exchange for `caching_sha2_password`
2+
3+
ProxySQL 3.1 can authenticate MySQL clients that use
4+
`caching_sha2_password` over a non-TLS frontend connection. When full
5+
authentication is required, the client can request ProxySQL's RSA public key,
6+
encrypt its password, and send the ciphertext back to ProxySQL.
7+
8+
TLS remains the recommended configuration. Requesting a public key over an
9+
unauthenticated connection encrypts the password on the wire, but it does not
10+
authenticate the ProxySQL server and is vulnerable to public-key substitution
11+
by an active network attacker. Use TLS when server identity and transport
12+
integrity are required.
13+
14+
## Configuration
15+
16+
The following MySQL variables are available in ProxySQL 3.1 and later:
17+
18+
| Variable | Default | Description |
19+
| --- | --- | --- |
20+
| `mysql-caching_sha2_password_auto_generate_rsa_keys` | `true` | Generate a 2048-bit RSA pair when both configured files are absent. |
21+
| `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. |
22+
| `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. |
23+
24+
Apply changes with:
25+
26+
```sql
27+
LOAD MYSQL VARIABLES TO RUNTIME;
28+
```
29+
30+
The three variables form one configuration unit. ProxySQL validates or
31+
generates the complete pair before publishing it to frontend sessions. If a
32+
reload fails, all three runtime values and the previously loaded key snapshot
33+
remain unchanged.
34+
35+
Relative paths must stay beneath ProxySQL's data directory. Empty, `.` and
36+
`..` components are rejected, and every parent directory is opened without
37+
following symbolic links. Absolute paths are allowed when keys are managed in
38+
another operator-controlled directory.
39+
40+
## Key formats and permissions
41+
42+
The private key must be an unencrypted PKCS#8 PEM RSA private key (the PEM
43+
header is `BEGIN PRIVATE KEY`). Traditional PKCS#1 (`BEGIN RSA PRIVATE KEY`)
44+
and encrypted private keys are rejected. The public key must be a PEM
45+
SubjectPublicKeyInfo public key. The two files must contain a structurally
46+
valid matching RSA pair of at least 2048 bits.
47+
48+
The private file must be a regular file and must not grant any group or other
49+
permissions. Generated files use these modes:
50+
51+
- private key: `0600`
52+
- public key: `0644`
53+
54+
Encrypted private keys are not supported because ProxySQL has no runtime
55+
passphrase input for this feature.
56+
57+
If the compiled default pair is unusable during initial runtime loading and
58+
cannot be regenerated safely, ProxySQL records an explicit TLS-only state
59+
(automatic generation off and both paths empty). TLS authentication remains
60+
available, while RSA public-key authentication stays disabled until a valid
61+
pair is loaded.
62+
63+
Automatic generation occurs only when both paths are absent. If exactly one
64+
file exists, ProxySQL reports a configuration error and does not overwrite or
65+
replace either path. Generation uses temporary files and no-overwrite
66+
publication so concurrent ProxySQL processes cannot publish a mixed pair.
67+
68+
## Reload and cluster behavior
69+
70+
Each authentication exchange retains the same immutable key snapshot from the
71+
public-key response through RSA decryption. A concurrent
72+
`LOAD MYSQL VARIABLES TO RUNTIME` can therefore rotate keys without breaking
73+
an exchange already in progress.
74+
75+
Cluster synchronization transfers the variable values, not private-key
76+
contents. Every ProxySQL node must be able to read its configured local pair,
77+
or generate its own pair when automatic generation is enabled. Do not store
78+
private-key contents in the ProxySQL configuration database.
79+
80+
## Client behavior and failures
81+
82+
The client must use `caching_sha2_password`, disable TLS only when intended,
83+
and enable its server-public-key request option. For Oracle's MySQL CLI:
84+
85+
```bash
86+
mysql --default-auth=caching_sha2_password \
87+
--ssl-mode=DISABLED --get-server-public-key \
88+
--host=127.0.0.1 --port=6033 --user=app --password
89+
```
90+
91+
ProxySQL implements the MySQL protocol's RSA OAEP exchange, including the
92+
protocol-defined SHA-1 OAEP and MGF1 digests and password/scramble XOR step.
93+
Malformed ciphertext, malformed plaintext, and an incorrect password all
94+
produce the normal `1045` / `28000` access-denied response. If no valid RSA key
95+
pair is available, the same error code and SQLSTATE are returned with a message
96+
that identifies the unavailable RSA key exchange and suggests TLS or key
97+
configuration.

doc/internal/passthrough_authentication.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,14 @@ Entry includes username, source IP, hostgroup probed, outcome. Useful for forens
279279

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

282-
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.
282+
ProxySQL 3.1 adds the frontend RSA public-key exchange for
283+
`caching_sha2_password`; see
284+
[`doc/caching_sha2_password_rsa.md`](../caching_sha2_password_rsa.md). This lets
285+
frontend users complete full authentication without TLS. Pass-through keeps
286+
its secure default (`mysql-passthrough_auth_require_tls=true`). If an operator
287+
explicitly disables that gate, the same RSA exchange can supply the cleartext
288+
credential used by the backend authentication probe; the public-key
289+
substitution warning in the linked document applies.
283290

284291
## 8. The cache
285292

include/MySQL_Caching_Sha2_RSA.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#ifndef PROXYSQL_MYSQL_CACHING_SHA2_RSA_H
2+
#define PROXYSQL_MYSQL_CACHING_SHA2_RSA_H
3+
4+
#include <memory>
5+
#include <mutex>
6+
#include <string>
7+
8+
#include <openssl/types.h>
9+
10+
struct CachingSha2RSAConfig {
11+
bool auto_generate { true };
12+
std::string private_key_path;
13+
std::string public_key_path;
14+
std::string datadir;
15+
};
16+
17+
class CachingSha2RSAKeySnapshot {
18+
public:
19+
const std::string& public_key_pem() const { return public_key_pem_; }
20+
size_t ciphertext_size() const { return ciphertext_size_; }
21+
22+
private:
23+
friend class MySQL_Caching_Sha2_RSA;
24+
std::shared_ptr<EVP_PKEY> private_key_;
25+
std::string public_key_pem_;
26+
std::string private_key_path_;
27+
std::string public_key_path_;
28+
size_t ciphertext_size_ { 0 };
29+
};
30+
31+
struct CachingSha2RSAReloadResult {
32+
bool accepted { false };
33+
bool changed { false };
34+
bool available { false };
35+
std::string error;
36+
};
37+
38+
class MySQL_Caching_Sha2_RSA {
39+
public:
40+
CachingSha2RSAReloadResult reload(const CachingSha2RSAConfig& config);
41+
std::shared_ptr<const CachingSha2RSAKeySnapshot> acquire() const;
42+
bool decrypt_password(
43+
const std::shared_ptr<const CachingSha2RSAKeySnapshot>& snapshot,
44+
const unsigned char* ciphertext,
45+
size_t ciphertext_length,
46+
const unsigned char* scramble,
47+
size_t scramble_length,
48+
std::string& password,
49+
std::string* error = nullptr
50+
) const;
51+
52+
private:
53+
mutable std::mutex mutex_;
54+
std::shared_ptr<const CachingSha2RSAKeySnapshot> snapshot_;
55+
};
56+
57+
#endif

include/MySQL_Passthrough_Auth_Cache.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class MySQL_Passthrough_Auth_Cache {
3737
private:
3838
struct entry_t {
3939
std::string cleartext_password;
40-
uint64_t learned_at_us;
41-
int hostgroup_probed;
40+
uint64_t learned_at_us { 0 };
41+
int hostgroup_probed { 0 };
42+
~entry_t();
4243
};
4344
mutable pthread_rwlock_t lock;
4445
std::unordered_map<std::string, entry_t> entries;
@@ -109,7 +110,7 @@ class MySQL_Passthrough_Auth_Cache {
109110
bool lookup(const std::string& username, std::string& out_cleartext, uint32_t ttl_s);
110111

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

114115
// Evict a single entry. Returns true if the entry was present.
115116
bool evict(const std::string& username);

include/MySQL_Protocol.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66
#include "MySQL_Variables.h"
77
#include "MySQL_Prepared_Stmt_info.h"
88

9+
#ifdef PROXYSQL31
10+
#include <memory>
11+
12+
class CachingSha2RSAKeySnapshot;
13+
14+
enum class MySQLFrontendAuthError : uint8_t {
15+
NONE = 0,
16+
CACHING_SHA2_RSA_UNAVAILABLE
17+
};
18+
#endif
19+
920
#define RESULTSET_BUFLEN 16300
1021

1122
extern MySQL_Variables mysql_variables;
@@ -112,6 +123,9 @@ class MyProt_tmp_auth_vars {
112123
uint8_t zstd_compression_level = 0;
113124
bool use_ssl = false;
114125
bool use_zstd_compression = false;
126+
#ifdef PROXYSQL31
127+
bool pass_is_sensitive = false;
128+
#endif
115129
enum proxysql_session_type session_type;
116130
};
117131

@@ -141,6 +155,10 @@ class MySQL_Protocol {
141155
enum proxysql_auth_plugins auth_plugin_id;
142156
uint16_t prot_status;
143157
bool more_data_needed;
158+
#ifdef PROXYSQL31
159+
std::shared_ptr<const CachingSha2RSAKeySnapshot> caching_sha2_rsa_snapshot_;
160+
MySQLFrontendAuthError frontend_auth_error_ { MySQLFrontendAuthError::NONE };
161+
#endif
144162
MySQL_Data_Stream *get_myds() { return *myds; }
145163
MySQL_Protocol()
146164
: userinfo(nullptr), sess(nullptr), myds(nullptr), current_PreStmt(nullptr)
@@ -214,6 +232,10 @@ class MySQL_Protocol {
214232
bool PPHR_verify_password_2(MyProt_tmp_auth_vars& vars1, account_details_t& account_details);
215233

216234
void generate_one_byte_pkt(unsigned char b);
235+
#ifdef PROXYSQL31
236+
void generate_auth_more_data(const unsigned char *data, size_t data_len);
237+
MySQLFrontendAuthError consume_frontend_auth_error();
238+
#endif
217239

218240
bool process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned int len);
219241
void * Query_String_to_packet(uint8_t sid, std::string *s, unsigned int *l);

include/MySQL_Thread.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include <sys/epoll.h>
1414
#endif // IDLE_THREADS
1515
#include <atomic>
16+
#include <memory>
17+
#include <string>
1618

1719
#include "prometheus_helpers.h"
1820

@@ -38,6 +40,14 @@
3840

3941
extern class MySQL_Variables mysql_variables;
4042

43+
#ifdef PROXYSQL31
44+
class MySQL_Caching_Sha2_RSA;
45+
#endif
46+
47+
struct MySQLThreadsCommitResult {
48+
unsigned int rejected_variables { 0 };
49+
};
50+
4151
#ifdef IDLE_THREADS
4252
typedef struct __attribute__((aligned(64))) _conn_exchange_t {
4353
pthread_mutex_t mutex_idles;
@@ -427,6 +437,13 @@ class MySQL_Threads_Handler
427437
// variable address
428438
// special variable : if true, further input validation is required
429439
std::unordered_map<std::string, std::tuple<bool *, bool>> VariablesPointers_bool;
440+
#ifdef PROXYSQL31
441+
std::unique_ptr<MySQL_Caching_Sha2_RSA> caching_sha2_rsa_manager_;
442+
bool caching_sha2_rsa_config_initialized_ { false };
443+
bool caching_sha2_rsa_accepted_auto_generate_ { true };
444+
std::string caching_sha2_rsa_accepted_private_path_;
445+
std::string caching_sha2_rsa_accepted_public_path_;
446+
#endif
430447
/**
431448
* @brief Holds the clients host cache. It keeps track of the number of
432449
* errors associated to a specific client:
@@ -521,6 +538,11 @@ class MySQL_Threads_Handler
521538
int select_version_forwarding;
522539
char *keep_multiplexing_variables;
523540
char *default_authentication_plugin;
541+
#ifdef PROXYSQL31
542+
bool caching_sha2_password_auto_generate_rsa_keys;
543+
char *caching_sha2_password_private_key_path;
544+
char *caching_sha2_password_public_key_path;
545+
#endif
524546
char *proxy_protocol_networks;
525547
//unsigned int default_charset; // removed in 2.0.13 . Obsoleted previously using MySQL_Variables instead
526548
int handle_unknown_charset;
@@ -790,11 +812,14 @@ class MySQL_Threads_Handler
790812
unsigned int get_global_version();
791813
void wrlock();
792814
void wrunlock();
793-
void commit();
815+
MySQLThreadsCommitResult commit();
794816
char *get_variable(char *name);
795817
bool set_variable(char *name, const char *value);
796818
char **get_variables_list();
797819
bool has_variable(const char * name);
820+
#ifdef PROXYSQL31
821+
MySQL_Caching_Sha2_RSA* caching_sha2_rsa() const { return caching_sha2_rsa_manager_.get(); }
822+
#endif
798823

799824
MySQL_Threads_Handler();
800825
~MySQL_Threads_Handler();

include/mysql_connection.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class MySQL_Connection_userinfo {
5555
char *fe_username;
5656
MySQL_Connection_userinfo();
5757
~MySQL_Connection_userinfo();
58+
void clear_password();
5859
void set(char *, char *, char *, char *);
5960
void set(MySQL_Connection_userinfo *);
6061
bool set_schemaname(char *, int);

lib/Admin_FlushVariables.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,11 @@ FlushVariableStats ProxySQL_Admin::flush_mysql_variables___database_to_runtime(S
574574
free(default_collation_connection);
575575
free(previous_default_charset);
576576
free(previous_default_collation_connection);
577-
GloMTH->commit();
577+
const MySQLThreadsCommitResult commit_result = GloMTH->commit();
578+
if (commit_result.rejected_variables != 0) {
579+
stats.updated = std::max(0, stats.updated - static_cast<int>(commit_result.rejected_variables));
580+
stats.rejected += static_cast<int>(commit_result.rejected_variables);
581+
}
578582
GloMTH->wrunlock();
579583

580584
{

lib/Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo
121121
Query_Processor_ParserSQL.oo \
122122
proxy_sqlite3_symbols.oo
123123

124+
ifeq ($(PROXYSQL31),1)
125+
_OBJ_CXX += MySQL_Caching_Sha2_RSA.oo
126+
endif
127+
124128
# TSDB object files
125129
ifeq ($(PROXYSQLTSDB),1)
126130
_OBJ_CXX += TSDB_Dashboard_html.oo

0 commit comments

Comments
 (0)