Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
48 changes: 48 additions & 0 deletions docs/superpowers/plans/2026-08-10-auth-methods-rsa-version-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Auth Methods RSA Version Gate Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make `test_auth_methods-t` select the correct RSA full-authentication expectations from the runtime ProxySQL version boundary at 3.1.

**Architecture:** Read and parse the ProxySQL Admin `SELECT @@version` result once, then pass the derived RSA capability through the existing authentication expectation functions. Keep the legacy failure oracle below 3.1 and recognize the RSA packet exchange at or above 3.1.

**Tech Stack:** C++17, MariaDB/MySQL C API, TAP test helpers.

## Global Constraints

- ProxySQL versions 3.1 and newer support non-TLS `caching_sha2_password` RSA full authentication.
- ProxySQL versions below 3.1 retain the legacy expected-failure behavior.
- The runtime ProxySQL version, not the TAP binary's compile flags, determines the expectation.
- Do not push the branch without explicit user approval.

---

### Task 1: Runtime version capability and authentication oracle

**Files:**
- Modify: `test/tap/tests/test_auth_methods-t.cpp`

**Interfaces:**
- Produces: `parse_proxysql_version(const std::string&, int&, int&) -> bool`.
- Produces: `supports_caching_sha2_rsa(int, int) -> bool`.
- Consumes: ProxySQL Admin `SELECT @@version` result and the derived `supports_rsa` flag.

- [ ] **Step 1: Write failing boundary tests**

Add TAP assertions with literal expectations for `2.7`, `3.0`, `3.1`, `4.0.11-113-g...`, negative components, and malformed input before defining the new helpers.

- [ ] **Step 2: Run the focused build and verify RED**

Run `make -C test/tap/tests test_auth_methods-t` with the branch's normal feature flags. Expect compilation to fail because the version helpers do not exist yet.

- [ ] **Step 3: Implement runtime detection and capability plumbing**

Implement strict leading major/minor parsing, query `SELECT @@version` on the Admin connection, and fail with a diagnostic on query/result/parse errors. Apply the derived boolean only to the pre-3.1 non-TLS hashed SHA-2 exception, expected success/failure counts, and RSA full-auth packet classification.

- [ ] **Step 4: Run focused verification and verify GREEN**

Rebuild `test_auth_methods-t`, run it against the local test environment, and confirm the TAP plan and all assertions pass. Run `git diff --check` and inspect the focused diff.

- [ ] **Step 5: Leave the verified changes local until publication is approved**

Report the exact files changed, ProxySQL version detected, commands run, and test results. Commit and push only after explicit user approval.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Auth Methods RSA Version Gate Design

## Goal

Make `test_auth_methods-t` expect non-TLS `caching_sha2_password` RSA full authentication to succeed only when the ProxySQL instance under test is version 3.1 or newer. ProxySQL versions older than 3.1 must retain the existing expected-failure behavior.

## Runtime capability detection

The test will query `SELECT @@version` through the already-established ProxySQL Admin connection. It will parse non-negative leading major and minor numeric components and derive one capability flag:

- `false` for versions below 3.1;
- `true` for versions 3.1 and newer.

The runtime version is authoritative. Compile-time flags are unsuitable because the TAP executable may be used against a separately built ProxySQL binary. Failure to query or parse the ProxySQL version will terminate the test with a diagnostic instead of silently selecting the wrong expectations.

## Authentication expectations

The existing exceptional-failure rule for non-TLS, hashed `caching_sha2_password` credentials will apply only when RSA full authentication is unavailable. On ProxySQL 3.1 and newer, valid credentials will follow the normal success path on their first attempt.

RSA full authentication adds one server packet for the public-key response. The session-packet classifier will recognize that exchange for RSA-capable ProxySQL versions so the existing full-auth assertions continue to describe the actual protocol rather than merely accepting the connection.

All unrelated authentication limitations and expectations remain unchanged.

## Verification

The TAP test will contain literal boundary checks covering pre-3.1, 3.1, later, suffixed, and malformed version strings. After the red/green cycle, the focused test binary will be built and run against the local ProxySQL test environment. The verified change will be pushed only after explicit user approval.
83 changes: 83 additions & 0 deletions include/MySQL_Caching_Sha2_RSA.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#ifndef __CLASS_MYSQL_CACHING_SHA2_RSA_H
#define __CLASS_MYSQL_CACHING_SHA2_RSA_H

#include <memory>
#include <pthread.h>
#include <string>

#include <openssl/types.h>

/**
* @brief Requested caching_sha2_password RSA key configuration.
* @details Private and public paths are configured together; relative paths resolve beneath @p datadir.
*/
struct MySQL_Caching_Sha2_RSA_Config {
bool auto_generate { true };
std::string private_key_path;
std::string public_key_path;
std::string datadir;
};

/** @brief Immutable RSA key material retained by authentication exchanges. */
class MySQL_Caching_Sha2_RSA_Key_Snapshot {
public:
/** @brief Return the canonical SPKI PEM public key supplied to a MySQL client. */
const std::string& public_key_pem() const { return public_key_pem_; }
/** @brief Return the exact RSA ciphertext size accepted by this snapshot. */
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 };
};

/** @brief Result of an RSA reload; a rejected reload preserves the active snapshot. */
struct MySQL_Caching_Sha2_RSA_Reload_Result {
/** @brief The requested configuration passed validation and was accepted. */
bool accepted { false };
/** @brief A newly prepared key snapshot replaced the previously active one. */
bool changed { false };
/** @brief An active snapshot is available after this reload attempt. */
bool available { false };
/** @brief Validation or preparation failure when @c accepted is false. */
std::string error;
};

/** @brief Atomically publishes immutable RSA snapshots for caching_sha2_password exchanges. */
class MySQL_Caching_Sha2_RSA {
public:
MySQL_Caching_Sha2_RSA() = default;
~MySQL_Caching_Sha2_RSA();
MySQL_Caching_Sha2_RSA(const MySQL_Caching_Sha2_RSA&) = delete;
MySQL_Caching_Sha2_RSA& operator=(const MySQL_Caching_Sha2_RSA&) = delete;
MySQL_Caching_Sha2_RSA(MySQL_Caching_Sha2_RSA&&) = delete;
MySQL_Caching_Sha2_RSA& operator=(MySQL_Caching_Sha2_RSA&&) = delete;

/** @brief Prepare and atomically publish a valid key pair without replacing a rejected snapshot. */
MySQL_Caching_Sha2_RSA_Reload_Result reload(const MySQL_Caching_Sha2_RSA_Config& config);
/** @brief Acquire a snapshot that remains valid even if a later reload publishes another one. */
std::shared_ptr<const MySQL_Caching_Sha2_RSA_Key_Snapshot> acquire() const;
/**
* @brief Decrypt an exact-size OAEP ciphertext containing one trailing-NUL password.
* @details On success, @p password contains cleartext and its caller must cleanse it after use.
*/
bool decrypt_password(
const std::shared_ptr<const MySQL_Caching_Sha2_RSA_Key_Snapshot>& 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 pthread_mutex_t mutex_ = PTHREAD_MUTEX_INITIALIZER;
std::shared_ptr<const MySQL_Caching_Sha2_RSA_Key_Snapshot> snapshot_;
};

#endif
18 changes: 14 additions & 4 deletions include/MySQL_Passthrough_Auth_Cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ struct passthrough_entry_view {

class MySQL_Passthrough_Auth_Cache {
private:
/** @brief Cache entry whose destructor cleanses its owned cleartext credential. */
struct entry_t {
std::string cleartext_password;
uint64_t learned_at_us;
int hostgroup_probed;
uint64_t learned_at_us { 0 };
int hostgroup_probed { 0 };
entry_t() = default;
~entry_t();
entry_t(const entry_t&) = delete;
entry_t& operator=(const entry_t&) = delete;
entry_t(entry_t&&) = delete;
entry_t& operator=(entry_t&&) = delete;
};
mutable pthread_rwlock_t lock;
std::unordered_map<std::string, entry_t> entries;
Expand Down Expand Up @@ -108,8 +115,11 @@ class MySQL_Passthrough_Auth_Cache {
// than ttl_s, the entry is evicted and a miss is returned.
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);
/**
* @brief Copy a non-null cleartext credential into the cache.
* @details Replacing an entry cleanses the previously owned credential.
*/
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
Loading
Loading