MySQL を 5.7 から 8 に上げた直後、アプリのコードも接続 URL も変えていないのに、Java の接続だけが落ちるようになります。
java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
HikariCP でプールを作る構成なら、この例外はプール初期化の中で投げられます。Spring Boot で起動時にプールを
初期化していれば ApplicationContext の起動失敗として現れ、この 1 行はスタックトレースの奥に入ります。
検索でコピペする文字列は出る位置によって変わるので、検証で実際に得たものを並べます。
- 例外クラス(Connector/J 8):
java.sql.SQLNonTransientConnectionException - メッセージ:
Public Key Retrieval is not allowed Caused by側:com.mysql.cj.exceptions.UnableToConnectException: Public Key Retrieval is not allowed- 止まった場所として出るフレーム:
com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin.nextAuthenticationStep
Caused by のフレームが示すとおり、止めているのは caching_sha2_password という認証プラグインです。
この経路に入る条件は 2 つあり、接続が暗号化されていないことと、サーバの認証キャッシュにそのアカウントが
載っていないことが同時に成り立ったときに出ます。URL の useSSL=false は前者を作る典型的な書き方です。
直し方の要点を先に
接続 URL から useSSL=false を消し、TLS を必須にします。 driver も MySQL の版もアプリのコードも動かしません。
# 変更前(5.7 時代の URL がそのまま残っている形)
jdbc:mysql://db:3306/appdb?useSSL=false&characterEncoding=utf8
# 変更後(TLS を必須にする)
jdbc:mysql://db:3306/appdb?sslMode=REQUIRED&characterEncoding=utf8
同じサーバ・同じ driver・同じコードで、URL のパラメータだけを変えた 4 通りの結果です。
| 接続 URL のパラメータ | 終了コード | 結果 | 接続後の Ssl_cipher |
|---|---|---|---|
useSSL=false | 1 | Public Key Retrieval is not allowed | —(接続できていない) |
useSSL=false&allowPublicKeyRetrieval=true | 0 | 接続成功 | 空(TLS 無し) |
sslMode=REQUIRED | 0 | 接続成功 | TLS_AES_256_GCM_SHA384 |
| パラメータ無し(driver の既定) | 0 | 接続成功 | TLS_AES_256_GCM_SHA384 |
sslMode を書かずに消すだけでも、このサーバでは TLS で繋がりました(表の最終行)。ただし
Connector/J 8.0.13 以降の既定は PREFERRED で、これはサーバが TLS を提供していなければ平文へ
フォールバックする値です。落ちなくなったことが暗号化されたことを意味しないので、REQUIRED を明示して
フォールバックを止めるほうが、あとから TLS を失っても気づけます。
REQUIRED はサーバ証明書を検証しません。 検証まで求めるなら、CA を配って VERIFY_CA(証明書を CA で検証)、
接続先のホスト名まで確かめるなら VERIFY_IDENTITY を使います。この 2 つは実測していません。
TLS を用意していないサーバでは、useSSL=false を消しても TLS を張れないので同じ経路に入るはずです
(この構成は測っていません)。そのときの選択肢は後述します。
なぜ MySQL 8 で出るのか:既定の認証方式が変わった
サーバの既定認証プラグインが、5.7 の mysql_native_password から 8.0 の caching_sha2_password に変わりました。
検証したサーバ(8.0.46)では @@default_authentication_plugin が caching_sha2_password で、そこで作った
ユーザーの mysql.user.plugin も caching_sha2_password です。
caching_sha2_password は、接続が安全かどうかで経路を変えます。MySQL のマニュアルはこう書き分けています——
接続が安全なら RSA の鍵ペアは不要(TLS で暗号化した TCP、Unix ソケット、共有メモリが該当し、パスワードは
平文で送られるが通信路が守る)。安全でなければ RSA の鍵ペアを使う(TLS でない TCP と名前付きパイプが該当し、
パスワードの盗み見を防ぐために RSA を使う)。
後者では、クライアントがサーバの RSA 公開鍵を手に入れる必要があります。その公開鍵をサーバから取り寄せる動作が
「public key retrieval」で、Connector/J は既定でこれを拒否します。拒否した結果が
Public Key Retrieval is not allowed です。
MySQL 5.7 では同じことが起きません。errfix は同じ URL・同じ driver(8.0.33)・同じ Java コードのまま
サーバだけを mysql:5.7.44 に替えて接続し、成功することを確認しています。5.7 のユーザーは
mysql_native_password なので、TLS が無くても公開鍵のやり取りが要らないためです。変えたのは
サーバの版だけで、driver も URL もコードも同一です。
useSSL=false が URL に残っているのは、多くの場合 5.7 時代の設定を引き継いだためです。この 1 語は
「TLS を設定していないサーバに繋ぐための指定」でしたが、サーバを 8 に上げた瞬間、同じ 1 語が
認証方式の前提を外す指定に変わります。
同じ URL が通ったり落ちたりする:キャッシュが結果を変える
caching_sha2_password の「caching」の部分が、環境差として現れます。サーバは認証に成功したアカウントについて
アカウント名とパスワードハッシュの組をメモリ上に保持し、次からは公開鍵のやり取りが要らない経路
(fast path)で認証します。マニュアルはこのキャッシュが、アカウントの削除・改名・認証情報の変更で該当分を消し、
FLUSH PRIVILEGES で全部空になり、**サーバ停止時にも空になる(再起動をまたいで残らない)**と書いています。
同じサーバ・同じユーザー・同じ URL(useSSL=false のみ)で、キャッシュの状態だけを変えた結果です。
1. 起動直後(このユーザーで一度も接続していない)
→ Public Key Retrieval is not allowed
2. mysql クライアントで同じユーザーとして接続(成功)
3. 1 と同じ JDBC URL で再接続
→ 接続成功
4. FLUSH PRIVILEGES のあと、同じ JDBC URL
→ Public Key Retrieval is not allowed
コードも URL も driver も同じで、サーバ側のキャッシュにそのアカウントが載っているかどうかだけで結果が変わります。 ここから、次の 3 つが同じ 1 つの現象として説明できます。
- CI でだけ落ちる:毎回新しい DB コンテナを起動するので、キャッシュは常に空です。
- 開発機では落ちない:以前に一度接続していれば、その DB が動いている間は通ります。
- 動いていたのに、ある日から落ちる:DB を再起動した、あるいは
FLUSH PRIVILEGESが走った後です。
「再現しない」と報告された不具合がこの形をしている場合、疑うのはアプリ側ではなくサーバのキャッシュの状態です。
allowPublicKeyRetrieval=true を足す前に
このエラーで最初に見つかる対処は allowPublicKeyRetrieval=true の追加で、実際に接続は通ります
(errfix の機械検証もこの修正で行っています)。ただし、これが有効にするのは
サーバが提示してきた公開鍵を、検証せずに受け取ってよいという動作です。Connector/J のドキュメントは、
このプロパティを「サーバから RSA 公開鍵を直接取得するための特別なハンドシェイク往復を許す」と説明し、
既定値を false としています。MySQL のマニュアルはこの 2 経路を比べて、信頼できるローカルの公開鍵のコピーを
使うほうが、サーバに公開鍵を要求するより安全であり、サーバへの要求は「安全なネットワーク環境では
受け入れられる場合がある」と書いています。上の表のとおり、useSSL=false と組み合わせた接続は
Ssl_cipher が空、つまり通信路自体が暗号化されていません。
平文で繋ぐしかない場合の選択肢は 2 つあります。
- サーバの公開鍵をクライアント側に置く:サーバのデータディレクトリにある
public_key.pemを配り、serverRSAPublicKeyFileにそのパスを渡します。マニュアルが「より安全」と書いているのはこちらです (この構成は測っていません)。 allowPublicKeyRetrieval=trueを足す:鍵の配布が要らない代わりに、接続のたびにサーバから鍵を受け取ります。 取り寄せる鍵が本物であることを、接続先を他の手段で信頼できる範囲に限って使います。
サーバ側で default_authentication_plugin を mysql_native_password に戻す案は、MySQL 8.0.27 で非推奨になり、
8.4.0 で削除されました(移行先は authentication_policy)。8.4 に同じ指定を渡すとサーバは unknown variable で
起動しません。さらに 8.4.0 からは mysql_native_password プラグイン自体が既定で無効で、使うには
--mysql-native-password=ON が要ります。5.7 の状態へ戻すのではなく接続側を 8 の既定に合わせるほうが、
次のサーバ更新で同じ移行をもう一度やらずに済みます。
なお useSSL そのものも Connector/J 8.0.13 で非推奨になり、現在の指定は sslMode です。URL を触るなら
useSSL=false を sslMode=DISABLED に書き換えるのではなく、TLS を切る指定を消す方向で検討してください。
切り分け
mysqlコマンドでは繋がるのに JDBC で落ちる:検証したサーバでも、CLI は同じユーザー・同じサーバに 接続できました。CLI で繋がったこと自体がキャッシュを温めるので、その直後に JDBC を試すと通ってしまい、 切り分けが崩れます。比較するなら、CLI を打つ前に JDBC を試してください。- 一部のユーザーだけ落ちる:
mysql.userのplugin列を見ます。5.7 から上げたサーバでは既存アカウントの プラグインは変わらないので、認証プラグインを指定せず 8.0 の既定のまま作ったアカウントだけがcaching_sha2_passwordになります(IDENTIFIED WITHで明示した場合や、サーバ側で既定を変えている場合は別です)。 判断の正本はSELECT user, host, plugin FROM mysql.user;の出力です。 - URL に
useSSL=falseが無いのに出る:サーバ側が TLS を提供していない可能性があります。既定のsslMode=PREFERREDは、TLS が張れないときは平文で接続するので、同じ経路に入ります。接続できたときはSHOW SESSION STATUS LIKE 'Ssl_cipher'の値が空かどうかで、その接続が暗号化されているかを判別できます。 - URL を直したのに変わらない:接続 URL が 2 か所以上にある構成(
application.ymlと CI の環境変数、 Flyway と JPA が別々の URL を持つ等)では、落ちている側だけが古いままのことがあります。 - エラーが
Access deniedに変わった:認証の次の段階まで進んでいます。ユーザー名・パスワード・ホスト ('appuser'@'%'の%の部分)に加えて、serverRSAPublicKeyFileで公開鍵ファイルを渡しているなら、その鍵が そのサーバのものかを確認します。マニュアルは、鍵ファイルの値が公開鍵として妥当でも中身が違えば access-denied になると書いています。 - PostgreSQL で似た症状が出た:サーバ側の認証方式が変わって JDBC が追随できない、という同じ形は PostgreSQL にもあります(The authentication type 10 is not supported の直し方)。 そちらは driver の版を上げて直します。
検証環境
- image:
mysql(digest 固定sha256:7dcddc01…= MySQL 8.0.46)。同じコンテナに JDK 17 を追加し、 MySQL サーバと JDBC クライアントを 1 コンテナ内で動かして127.0.0.1へ接続 - driver:
com.mysql:mysql-connector-j:8.0.33(Maven Central から取得) - 接続するユーザーは 8.0 の既定のもとで作成し、
mysql.user.pluginがcaching_sha2_passwordであることを確認。 セットアップ中に一度も接続させていない(接続するとキャッシュが温まり、再現しなくなるため) - 再現:
jdbc:mysql://127.0.0.1:3306/appdb?useSSL=false&characterEncoding=utf8→ 上記の例外・終了コード 1 - 修正:同じ URL に
&allowPublicKeyRetrieval=trueを足す →SELECT 1まで通って終了コード 0。 reproduce と fix で違うのは URL のパラメータ 1 つだけで、サーバ・driver・ユーザー・接続コードは同一 - 上の 4 通りの表と、キャッシュの状態を変えた 3 回の接続は、同じイメージで追加で測ったものです。
fail-closed のゲート(再現 → 修正 → シグネチャ消滅)を通しているのは
allowPublicKeyRetrieval=trueの側で、sslMode=REQUIREDとパラメータ無しの 2 つは、この記事のための測定です - 5.7 との対照(同じ URL・同じ driver・同じコードで
mysql:5.7.44に接続成功)は、DB レーンを開いたときの測定によります - このサーバでは TLS が有効でした(接続後の
Ssl_cipherに暗号スイートが入ることで確認)。TLS を用意していない サーバでの挙動、VERIFY_CA/VERIFY_IDENTITY、serverRSAPublicKeyFile、HikariCP を挟んだ場合の発現箇所は 測っていません
再現から修正までは errfix の検証ハーネスが機械的に確認しています。既定認証プラグインの変更、安全な接続と
そうでない接続で RSA 鍵ペアを使い分けること、キャッシュがアカウント名とパスワードハッシュの組を保持し
FLUSH PRIVILEGES とサーバ停止で空になること、ローカルの公開鍵のほうが安全と案内されていること、
鍵の不一致が access-denied になることは、MySQL の caching_sha2_password のマニュアル。
sslMode の値と既定(PREFERRED・8.0.13 から)、allowPublicKeyRetrieval と serverRSAPublicKeyFile の説明は
Connector/J の接続プロパティのドキュメント。default_authentication_plugin の非推奨(8.0.27)と削除(8.4.0)、
mysql_native_password が 8.4.0 から既定で無効であることは MySQL 8.4 の変更点で、それぞれ確認しています。