Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8a6989e
docs: add design spec for MariaDB ed25519 client authentication
renecannao Aug 11, 2026
21c4fc0
docs: implementation plan for MariaDB ed25519 authentication
renecannao Aug 11, 2026
fd345e1
build: statically link client_ed25519 connector plugin, add PROXYSQLE…
renecannao Aug 11, 2026
9aced7f
feat: add MariaDB-variant Ed25519 helpers with known-answer unit tests
renecannao Aug 11, 2026
3d237de
feat: frontend client_ed25519 authentication via Auth Switch
renecannao Aug 11, 2026
3ea8179
fix: NUL-terminate scramble_buff after the ed25519 nonce write
renecannao Aug 11, 2026
e45d060
feat: COM_CHANGE_USER support for ed25519 users, $ED$ load-time valid…
renecannao Aug 11, 2026
5295e30
fix: stop the ed25519 nonce from clobbering the connection's native s…
renecannao Aug 11, 2026
e032fc2
test: end-to-end MariaDB ed25519 authentication TAP test
renecannao Aug 11, 2026
b93f01d
test: assert specific errno/message for $ED$ backend-auth failure
renecannao Aug 11, 2026
c217f7d
docs: MariaDB ed25519 authentication guide
renecannao Aug 11, 2026
2b85860
fix: deny malformed $ED$-prefixed credentials, never fall back to cle…
renecannao Aug 11, 2026
5780da2
fix: scope ed25519 auth gates to MySQL sessions; brace NUL guard; zer…
renecannao Aug 11, 2026
019d410
test: exercise the ed25519 client-switch and forced change-user paths
renecannao Aug 11, 2026
500d815
docs: clarify default_authentication_plugin requirement and 3.0 upgra…
renecannao Aug 11, 2026
38ce7b1
fix: extend $ED$ fail-closed denial to SQLite3-server sessions
renecannao Aug 11, 2026
33bdf5f
fix: address bot-review findings on the ed25519 auth paths
renecannao Aug 11, 2026
9df1b97
test: harden the ed25519 e2e fixture and satisfy the Sonar security gate
renecannao Aug 11, 2026
88a91c0
docs: document the $ED$-additional-credential limitation, markdown po…
renecannao Aug 11, 2026
22ca75e
Merge remote-tracking branch 'origin/v3.0' into feature/ed25519-auth
renecannao Aug 11, 2026
8bb1880
style: drop unnecessary const-removing cast in verify_user_pass ed255…
renecannao Aug 11, 2026
293c086
style: resolve remaining SonarCloud new-code findings
renecannao Aug 11, 2026
0a593d0
ci: retrigger full check matrix
renecannao Aug 11, 2026
7b1e068
fix: exact-match ed25519 plugin-name negotiation; doc accuracy fixes
renecannao Aug 11, 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
14 changes: 8 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,18 @@ lint-tests:
### * GenAI plugin (built alongside core)
### - Automatically increments the major version (e.g., 3.0.6 -> 4.0.6).
###
### HIERARCHY: `PROXYSQL40=1` implies `PROXYSQL31=1` implies `PROXYSQLFFTO=1` + `PROXYSQLTSDB=1`.
### HIERARCHY: `PROXYSQL40=1` implies `PROXYSQL31=1` implies `PROXYSQLFFTO=1` + `PROXYSQLTSDB=1` + `PROXYSQLED25519=1`.

# If PROXYSQL40 is enabled, it automatically enables PROXYSQL31
ifeq ($(PROXYSQL40),1)
PROXYSQL31 := 1
endif

# If PROXYSQL31 is enabled, it automatically enables FFTO and TSDB
# If PROXYSQL31 is enabled, it automatically enables FFTO, TSDB and ED25519
ifeq ($(PROXYSQL31),1)
PROXYSQLFFTO := 1
PROXYSQLTSDB := 1
PROXYSQLED25519 := 1
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
endif

# Only increment version at the top-level make to avoid double-incrementing in recursive makes
Expand Down Expand Up @@ -106,6 +107,7 @@ export PROXYSQL40
export PROXYSQL31
export PROXYSQLFFTO
export PROXYSQLTSDB
export PROXYSQLED25519

### NOTES:
### SOURCE_DATE_EPOCH is used for reproducible builds
Expand Down Expand Up @@ -413,21 +415,21 @@ build_deps_debug_default:

.PHONY: build_lib_default
build_lib_default: build_deps_default
cd lib && OPTZ="${O2} -ggdb" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE}
cd lib && OPTZ="${O2} -ggdb" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) PROXYSQLED25519=$(PROXYSQLED25519) CC=${CC} CXX=${CXX} ${MAKE}

.PHONY: build_lib_debug_default
build_lib_debug_default: build_deps_debug_default
cd lib && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE}
cd lib && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) PROXYSQLED25519=$(PROXYSQLED25519) CC=${CC} CXX=${CXX} ${MAKE}

.PHONY: build_src_default
build_src_default: build_lib_default
cd src && OPTZ="${O2} -ggdb" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE}
cd src && OPTZ="${O2} -ggdb" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) PROXYSQLED25519=$(PROXYSQLED25519) CC=${CC} CXX=${CXX} ${MAKE}
$(if $(filter 1,$(PROXYSQL40)),cd plugins/mysqlx && OPTZ="${O2} -ggdb" PROXYSQL40=$(PROXYSQL40) PROXYSQL31=$(PROXYSQL31) PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE},@echo "[skip] mysqlx plugin (PROXYSQL40 not set)")
$(if $(filter 1,$(PROXYSQL40)),cd plugins/genai && OPTZ="${O2} -ggdb" PROXYSQL40=$(PROXYSQL40) PROXYSQL31=$(PROXYSQL31) PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE},@echo "[skip] genai plugin (PROXYSQL40 not set)")

.PHONY: build_src_debug_default
build_src_debug_default: build_lib_debug_default
cd src && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE}
cd src && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQLCLICKHOUSE=1 PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) PROXYSQLED25519=$(PROXYSQLED25519) CC=${CC} CXX=${CXX} ${MAKE}
$(if $(filter 1,$(PROXYSQL40)),cd plugins/mysqlx && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQL40=$(PROXYSQL40) PROXYSQL31=$(PROXYSQL31) PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE},@echo "[skip] mysqlx plugin (PROXYSQL40 not set)")
$(if $(filter 1,$(PROXYSQL40)),cd plugins/genai && OPTZ="${O0} -ggdb -DDEBUG" PROXYSQL40=$(PROXYSQL40) PROXYSQL31=$(PROXYSQL31) PROXYSQLFFTO=$(PROXYSQLFFTO) PROXYSQLTSDB=$(PROXYSQLTSDB) CC=${CC} CXX=${CXX} ${MAKE},@echo "[skip] genai plugin (PROXYSQL40 not set)")

Expand Down
9 changes: 9 additions & 0 deletions deps/mariadb-client-library/plugin_auth_CMakeLists.txt.patch
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ diff --git plugins/auth/CMakeLists.txt plugins/auth/CMakeLists.txt
index 83e324b9..7c4ed019 100644
--- plugins/auth/CMakeLists.txt
+++ plugins/auth/CMakeLists.txt
@@ -55,7 +55,7 @@
REGISTER_PLUGIN(TARGET client_ed25519
TYPE MARIADB_CLIENT_PLUGIN_AUTH
CONFIGURATIONS DYNAMIC STATIC OFF
- DEFAULT DYNAMIC
+ DEFAULT STATIC
SOURCES ${CC_SOURCE_DIR}/plugins/auth/ed25519.c
${REF10_SOURCES}
${CRYPT_SOURCE}
@@ -77,7 +77,7 @@ IF(CRYPTO_PLUGIN)
REGISTER_PLUGIN(TARGET caching_sha2_password
TYPE MARIADB_CLIENT_PLUGIN_AUTH
Expand Down
101 changes: 101 additions & 0 deletions doc/ed25519_authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# MariaDB ed25519 Authentication

ProxySQL supports MariaDB's ed25519 authentication scheme
(`client_ed25519` client plugin / `auth_ed25519` server plugin) on both
sides of the proxy.

## Availability

| Side | Tier | Mechanism |
|------|------|-----------|
| Backend (ProxySQL → MariaDB) | all tiers | The bundled MariaDB Connector/C links `client_ed25519` statically and answers the server's auth switch transparently. |
| Frontend (client → ProxySQL) | v3.1+ (`PROXYSQL31`) | ProxySQL verifies `client_ed25519` signatures itself. |

Oracle MySQL has no ed25519 plugin; this is a MariaDB-ecosystem feature.

## Credential formats in `mysql_users.password`

| Format | Example | Frontend auth | Backend auth |
|--------|---------|---------------|--------------|
| cleartext | `my_password` | yes (key derived on the fly) | yes (connector signs with it) |
| `$ED$` + 43-char base64 public key | `$ED$ZIgUREUg5PVgQ6LskhXmO+eZLS0nC8be6HPjYWR4YJY` | yes (signature verified against the key) | **no** — the password is unknown |

The `$ED$` payload is exactly the value MariaDB stores in
`mysql.user.authentication_string` for an ed25519 user — to migrate,
prefix it with `$ED$`. The prefix is case-insensitive and mandatory: a
bare 43-character string is treated as a cleartext password.

A malformed `$ED$` value (wrong length or invalid base64) logs a warning
at `LOAD MYSQL USERS TO RUNTIME` time and every authentication attempt
for that user fails with the standard access-denied error.

## Protocol behavior

ed25519 is never advertised in the initial handshake (its challenge is
32 bytes; the greeting scramble is 20). ProxySQL sends an
`AuthSwitchRequest` naming `client_ed25519` with a fresh 32-byte nonce
whenever:

- the stored credential is `$ED$…` (whatever plugin the client offered), or
- the client explicitly requested `client_ed25519` and the stored
credential is cleartext or `$ED$`.

The client answers with a 64-byte signature. This mirrors MariaDB's own
behavior, so any client able to authenticate against MariaDB ed25519
works unchanged. `COM_CHANGE_USER` into an ed25519 user is supported via
the same auth-switch mechanism.

TLS is not required: the exchange never transmits a secret.

**Frontend ed25519 requires `mysql-default_authentication_plugin=mysql_native_password`**
(this is ProxySQL's built-in default, so no change is needed unless it was
overridden). If it is set to `caching_sha2_password` instead, ProxySQL
advertises `caching_sha2_password` in its initial handshake greeting, and an
ordinary client — one that has not explicitly requested `client_ed25519` —
switches early to `caching_sha2_password` before ProxySQL has a chance to
route it into the ed25519 exchange. On that early-switch path, a `$ED$`
stored user is denied unconditionally (see Limitations below): it never
reaches the ed25519 verification code at all.

## Upgrading from 3.0

The `$ED$` prefix becomes reserved as of this feature: any stored
`mysql_users.password` value that literally begins with `$ED$` is now
parsed as an ed25519 credential, never compared as cleartext. The
reservation is **case-insensitive** — `$ed$`, `$Ed$` and `$eD$` count too
(matching how ProxySQL already detects the `$A$0` caching_sha2 format
case-insensitively). If an
existing 3.0 deployment happens to have a cleartext password that starts
with those four characters in any case combination (coincidental, but
possible), that account stops authenticating after the upgrade — this is
fail-closed by design (human-approved: silently falling back to cleartext
comparison for an unparseable "$ED$..." value was judged more dangerous
than a hard failure). ProxySQL warns once at `LOAD MYSQL USERS TO
RUNTIME` time for a malformed `$ED$` value, and once per user when a
backend connection is attempted with a `$ED$` credential. Fix by renaming
the credential to not start with `$ED$`, or by re-issuing it as a proper
`$ED$<public-key>` ed25519 credential if that was the intent.

## Limitations

- `$ED$` (public-key-only) users cannot open backend connections: the
signature scheme is not replayable and the cleartext is unknown.
ProxySQL logs an explicit warning when such a user's backend
connection fails. Store the cleartext password for full functionality.
- Pass-through authentication (`mysql-passthrough_auth_*`) cannot learn
credentials from an ed25519 exchange, by construction.
- If a client triggers an early switch to `mysql_native_password`
(e.g. it offered `caching_sha2_password` against a native greeting),
a stored-`$ED$` user cannot be verified on that connection — the
MySQL protocol allows a single auth switch. Standard MariaDB clients
do not hit this.
- A `$ED$` public key stored as the *additional* password (the
`additional_password` attribute) while the primary credential is an
ordinary password requires the client to explicitly request
`client_ed25519` (e.g. `--default-auth=client_ed25519`). ProxySQL
decides the auth switch from the primary credential and the client's
requested plugin; it deliberately does not force every client of such
an account through ed25519, because that would break clients without
the `client_ed25519` plugin whose primary credential is perfectly
valid.
- MariaDB PARSEC (11.6+) is not supported.
Loading
Loading