# Why does MySQL say public key retrieval is not allowed?

> **Quick answer:** Your connection has TLS turned off, and the
> `caching_sha2_password` plugin will not send the password in clear text. It
> would use the server's RSA public key instead, and fetching that key over an
> unencrypted connection is disabled because a man in the middle could swap it.
> Turn TLS on and verify the certificate. `sslMode=VERIFY_IDENTITY` is the
> setting; `REQUIRED` only encrypts, and the default, `PREFERRED`, can fall back
> to no encryption.

## What is the problem?

The message comes from MySQL Connector/J and reads:

```
java.sql.SQLNonTransientConnectionException:
Public Key Retrieval is not allowed
```

This is an authentication failure, not a network or privilege failure. The
wording can send people to firewall rules and grants.

Three facts explain it:

1. **`caching_sha2_password` is the plugin your account got.** It has been the
   default since 8.0.4, and on a stock `mysql:8.4.11` container, 2026-09-20, a
   plain `CREATE USER ... IDENTIFIED BY` produced exactly that plugin. The
   plugin assigned to a new account is governed by `authentication_policy`,
   which reads `*,,` on 8.0.46, 8.4.11 and 9.7.2, where `*` means the server
   default.
2. **It refuses to send a password over an unencrypted connection**, unless it
   can encrypt it with the server's RSA public key first.
3. **The client will not ask the server for that key over an unencrypted
   connection**, because it cannot distinguish the real key from a substitute.

An unencrypted connection is narrower than it sounds. From
[Caching SHA-2 Pluggable Authentication in the MySQL 8.4 manual](https://dev.mysql.com/doc/refman/8.4/en/caching-sha2-pluggable-authentication.html),
read 2026-09-20: "If the connection is secure, an RSA key pair is unnecessary and
is not used. This applies to TCP connections encrypted using TLS, as well as Unix
socket-file and shared-memory connections." A socket connection therefore does
not produce this error, which is why the problem reproduces from your
application but not from a `mysql` session inside the container.

The three conditions are this plugin, an insecure transport, and no key the
client already trusts. The bench below removes the second and third conditions,
and each was enough on its own. It does not remove the first. On a default
MySQL 8.4.11 server, 2026-09-20, you cannot use
`CREATE USER ... IDENTIFIED WITH mysql_native_password` because it returns
`ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded` unless the
server was started with `--mysql-native-password=ON`, which is covered at the
end of this page.

The bench also shows that the client cannot always remove a condition. On that
same MySQL 8.4.11 container, 2026-09-20, with
`require_secure_transport = ON`, an empty cache and
`--ssl-mode=DISABLED --get-server-public-key` returned
`ERROR 3159 (HY000): Connections using insecure transport are prohibited while
--require_secure_transport=ON.`

## How do I confirm it?

Check the account's plugin:

```sql
SELECT user, host, plugin
FROM mysql.user
WHERE user = 'app_user';
```

`caching_sha2_password` there is the first condition.

Then check your connection string for the second. In Connector/J the property is
`sslMode`, and `sslMode=DISABLED` produces this result. You may also see
`useSSL=false`, which does the same thing and which the vendor marks
"DEPRECATED: See 'sslMode' property description for details"
([Connector/J security properties](https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html),
read 2026-09-20). They are not equivalent spellings. Keep one `sslMode` policy
and remove the other property instead of setting both.

Older tutorials recommend `useSSL=false` to silence a certificate warning. That
parameter is a common cause of this problem.

Reproduce it from the command line, which gives a clearer error than the Java
one. On a stock `mysql:8.4.11` container, 2026-09-20, over TCP to an account
created with no plugin named, with the authentication cache emptied first:

```
mysql -h 127.0.0.1 -P 3306 --protocol=TCP -u pk -p --ssl-mode=DISABLED -e "SELECT 1;"

ERROR 2061 (HY000): Authentication plugin 'caching_sha2_password'
                    reported error: Authentication requires secure connection.
```

Remove `--ssl-mode=DISABLED` and the same command returned the row, on that same
MySQL 8.4.11 container and day. Asking that session
`SHOW STATUS LIKE 'Ssl_cipher'` reported `TLS_AES_128_GCM_SHA256`, which was the
difference between the two connections.

Two checks do not work on the versions covered here.
`SHOW VARIABLES LIKE 'have_ssl'` returns a row on 8.0.46 and returns nothing at
all on 8.4.11 and 9.7.2. `default_authentication_plugin` is also gone on both of
those versions. On 8.4, ask the performance schema instead:

<!-- sql-check: skip this table is not present on the MySQL 8.4 check server's configuration -->
```sql
SELECT CHANNEL, PROPERTY, VALUE
FROM performance_schema.tls_channel_status
WHERE PROPERTY = 'Enabled';
```

On `mysql:8.4.11`, 2026-09-20, that returned `mysql_main / Enabled / Yes`.

## How do I fix it?

### Turn TLS on

This is the fix. A server built against OpenSSL generates the TLS and RSA files
at first start when `auto_generate_certs` is on and no SSL options are
configured. On a stock `mysql:8.4.11` container, 2026-09-20, with nothing
configured, `auto_generate_certs` and
`caching_sha2_password_auto_generate_rsa_keys` both read `ON`, and the data
directory held `ca.pem`, `server-cert.pem`, `server-key.pem`, `public_key.pem`
and `private_key.pem`. A managed service, hardened image or inherited data
directory can differ, so read the variables instead of assuming.

An auto-generated certificate still has to be verified. Distribute it as a trust
anchor or replace it. Name the level you want:

```
# production: encrypted, and the server is who it claims to be
jdbc:mysql://db.example.internal:3306/app?sslMode=VERIFY_IDENTITY

# transitional only: encrypted, endpoint not verified
jdbc:mysql://db.internal:3306/app?sslMode=REQUIRED
```

The levels are not interchangeable. The difference between the middle two is
often misunderstood. From the
[Connector/J security properties](https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html),
read 2026-09-20: `REQUIRED` is "Establish secure connections if the server
enabled them, fail otherwise"; `VERIFY_CA` is "Like REQUIRED but additionally
verify the server TLS certificate against the configured Certificate Authority
(CA) certificates"; `VERIFY_IDENTITY` is "Like VERIFY_CA, but additionally
verify that the server certificate matches the host to which the connection is
attempted." Thus, `VERIFY_CA` alone accepts a validly issued certificate for
some other hostname. Only `VERIFY_IDENTITY` checks that it is your server.

`REQUIRED` stops eavesdropping but does not verify the server's identity. Treat it
as a step toward `VERIFY_IDENTITY`, not as the final setting.

**Do not rely on the default.** The
[Connector/J security properties](https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html),
read 2026-09-20, give `sslMode` a default of `PREFERRED`, documented as
"Establish encrypted connections if the server enabled them, otherwise fall back
to unencrypted connections". A connection string with no
`sslMode` at all can therefore use clear text without failing.

From the command line client the same levels are `--ssl-mode=REQUIRED`,
`VERIFY_CA` and `VERIFY_IDENTITY`.

Confirm that the connection is encrypted:

```sql
SHOW SESSION STATUS LIKE 'Ssl_cipher';
```

A non-empty value shows that TLS is in use. An empty one means TLS is off,
whatever the connection string says.

### The flag that works and that you should not use

```
allowPublicKeyRetrieval=true
```

This tells the client to accept whatever public key the server offers over the
unencrypted connection. The connection then succeeds, which is why this is a
common answer.

It removes the protection that the restriction provides. Anything between the
client and server can offer its own key, decrypt the password with the matching
private key and pass the connection on. Neither the client nor the server
detects this.

Loopback to a container on your own machine lowers the risk but does not remove
it. Malware, a debug proxy or a logging tool could still observe traffic on the
machine, and the connection string may later be used outside the loopback
environment. Scope it to a disposable local database, not to
"development".

### The third option, for completeness

You can put the server's public key on the client in advance, with
`serverRSAPublicKeyFile` in Connector/J or `--server-public-key-path` on the
command line. It works. On a stock `mysql:8.4.11` container, 2026-09-20, with the
authentication cache emptied first, `--ssl-mode=DISABLED
--server-public-key-path=/var/lib/mysql/public_key.pem` connected, while the same
command without that one flag and with the cache emptied again returned
`ERROR 2061`. The flag is the only difference between the two.

This protects the password during authentication but does not mean that TLS is
unnecessary. The successful connection reported an empty `Ssl_cipher`. The
pinned key does not encrypt your statements, result sets or session, and it does
not authenticate the server beyond that exchange. It is an option for
authentication in an environment that cannot use TLS, but it is not a
replacement for TLS.

Pin the key through a trusted channel. A public key fetched over the same
unencrypted connection gives no more protection than
`allowPublicKeyRetrieval`.

### Which option to pick

These options provide different protections:

| setting | password during authentication | server identity | your SQL and results |
|---|---|---|---|
| `sslMode=VERIFY_IDENTITY` | protected | verified, including the hostname | encrypted |
| `sslMode=VERIFY_CA` | protected | chain verified, hostname not | encrypted |
| `sslMode=REQUIRED` | protected from a passive listener | not verified | encrypted |
| `serverRSAPublicKeyFile`, no TLS | protected, if the key was pinned over a trusted channel | not verified | in clear text |
| `allowPublicKeyRetrieval=true`, no TLS | not protected against an active interceptor | not verified | in clear text |

Use the first row in production. Use the fourth only where TLS is genuinely
impossible and only for the authentication exchange. Use the last only against a
database you would be willing to throw away.

## Why does it work sometimes and not others?

`caching_sha2_password` has a cache, as its name indicates. Measured on a stock
MySQL 8.4.11 container, 2026-09-20, over TCP, one account, in this order:

| step | authentication cache | result |
|---|---|---|
| `--ssl-mode=DISABLED` | emptied first | `ERROR 2061` |
| default TLS | still cold for this account | connected, `Ssl_cipher` `TLS_AES_128_GCM_SHA256` |
| `--ssl-mode=DISABLED`, immediately after | warm | connected |
| `--ssl-mode=DISABLED`, after `FLUSH PRIVILEGES` | emptied again | `ERROR 2061` |

The last row explains the third one. The same command fails again once the cache is
emptied, so the successful connection used the fast path rather than one that
would have worked regardless.

**The cache is per account, not per server.** Entries are keyed by identity and
password: "Entries consist of account-name/password-hash pairs"
([Caching SHA-2 Pluggable Authentication, MySQL 8.4 manual](https://dev.mysql.com/doc/refman/8.4/en/caching-sha2-pluggable-authentication.html),
read 2026-09-20). Two accounts on the same server, same container and day:
after `pk2` authenticated over TLS, `pk` over `--ssl-mode=DISABLED` still
returned `ERROR 2061`, while `pk2` over `--ssl-mode=DISABLED` connected. Another
user connecting does nothing for you unless they used your account and password.

Changing the password empties that account's entry too. On the same container and
day, with `pk` warm, an `ALTER USER ... IDENTIFIED BY` put the next
`--ssl-mode=DISABLED` connection back to `ERROR 2061`.
[The same manual page](https://dev.mysql.com/doc/refman/8.4/en/caching-sha2-pluggable-authentication.html),
read 2026-09-20, lists the rest:
"It clears the cache entry for any account that is renamed or dropped, or any
account for which the credentials or authentication plugin are changed. It
empties the cache when the `FLUSH PRIVILEGES` statement is executed. It empties
the cache at server shutdown."

The intermittency is explained, but it is not a fix. A connection that works
because the cache is warm can fail after a restart, and it sends statements in
clear text throughout the session.

You can clear it deliberately to test:

<!-- sql-check: skip this clears the authentication cache on the shared check server, which would affect other connections -->
```sql
FLUSH PRIVILEGES;
```

## Why does AI-generated code cause this?

The generated connection string is the problem, and it was generated for the
environment its training data described. `useSSL=false` was standard advice for
years because Connector/J 5.1 warned about establishing an SSL connection
without server identity verification against MySQL 5.7 and other recent servers.
The fastest way to silence that warning was to turn SSL off.

A model asked for a MySQL JDBC URL may therefore produce one with TLS disabled.
That works with `mysql_native_password` and fails with the plugin that has been
the default since 8.0.

The other common pattern is the proposed fix. Asked to resolve "Public Key
Retrieval is not allowed", the answer that appears often in training data is
`allowPublicKeyRetrieval=true`, because it is the shortest change that makes the
error stop.

## How do I stop it coming back?

**Grep your configuration for `useSSL=false` and `allowPublicKeyRetrieval`**,
and treat both as findings rather than settings.

**Assert `Ssl_cipher` is non-empty in a startup check**, so a connection that
falls back to plain text fails visibly.

**Keep connectors current.** An old client that cannot speak
`caching_sha2_password` reports a different error, usually that the plugin could
not be loaded rather than anything about a public key. The fix is the driver,
and downgrading the account is a worse answer than it looks. The old plugin is
still in the 8.4 server and off: the MySQL 8.4 manual says it "is deprecated as
of MySQL 8.0.34, disabled by default in MySQL 8.4, and removed as of MySQL
9.0.0", and that "the server-side plugin is built into the server, but is
disabled by default. To enable it, start the MySQL Server with
`--mysql-native-password=ON`"
([manual](https://dev.mysql.com/doc/refman/8.4/en/native-pluggable-authentication.html)).
The server behaves that way. On MySQL 8.4.11 containers, 2026-09-20, a default
server reported `PLUGIN_STATUS` of `DISABLED` and
`CREATE USER ... IDENTIFIED WITH mysql_native_password` returned
`ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded`, while the
same image started with `--mysql-native-password=ON` reported it `ACTIVE`,
created the account and let it connect over TCP with `--ssl-mode=DISABLED` and
no key. The reason not to use it is the version after this one: MySQL 9.7.2
refused to start with the same option, logging `unknown variable
'mysql-native-password=ON'` and aborting. The escape hatch requires a server
restart now and blocks an upgrade later.

**Use `VERIFY_IDENTITY` where you can, and `VERIFY_CA` at the least.** Encryption
without verification still lets an impersonating server through, which is the
risk this error is meant to prevent.

**Enforce it on the server, not only in the URL.** A connection string can be
copied, overridden by a framework or edited by someone in a hurry.
`require_secure_transport = ON` makes a non-TLS connection fail instead of
quietly working. On MySQL 8.4.11 in a container, 2026-09-20, with the setting
on, all three routes shown here returned
`ERROR 3159 (HY000): Connections using insecure transport are prohibited while
--require_secure_transport=ON.`: `--get-server-public-key`, a pinned
`--server-public-key-path`, and an account whose cache entry was already warm.
Turning the setting off and running the same commands again connected, so the
rejection comes from the setting rather than the commands.

## How DBGorilla helps

DBGorilla connects read-only over a connection you configure, so the TLS
settings on this page are settings you already control rather than something the
tool works around. What it adds is the schema, table and index sizes, the workload by query
fingerprint, and activity samples carrying the account behind each session,
given to your AI coding agent, plus query plans. It surfaces and explains; it does not
write to your database.
[Get started free →](https://app.dbgorilla.com/signup)

## Related

- [How to fix too many connections in MySQL](https://www.dbgorilla.com/learn/mysql/how-to-fix-too-many-connections-in-mysql/)
- [How to set up a read-only MySQL user for an AI agent](https://www.dbgorilla.com/learn/mysql/how-to-set-up-a-read-only-mysql-user-for-an-ai-agent/)