【Java17】突然のSSLHandshakeExceptionを解決。原因はマイナーアップデートによる「暗号スイートの制限」だった
Java環境をアップデートした後、今まで正常に通信できていた外部APIとの接続で突然 SSLHandshakeException: Received fatal alert: handshake_failure が発生することはありませんか?
一見、接続先サーバーやネットワークの不具合に見えますが、実はJava17のマイナーアップデートによる「セキュリティの厳格化」が原因である場合があります。
今回は、原因の切り分けから解決に至るまでの実録をまとめました。
1. 事象:OSとJavaの更新後に通信不能に
開発環境や本番環境のベースイメージを更新した直後、特定の外部エンドポイントとの通信で以下のエラーがログに記録されました。
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure接続先のサーバー側から「ハンドシェイク失敗」として通信を即座に切断されている状態です。直前まで疎通できていたことから、インフラ的な要因よりも「環境更新による通信プロトコルのミスマッチ」が疑われました。
2. 原因切り分けのためのデバッグ実行
まず、ネットワークの問題なのか、Java固有の問題なのかを切り分けるために以下の確認を行いました。
curlでの疎通確認(OSレベルの確認)
Podやコンテナ内に curl をインストールし、OSレベルでの疎通を確認します。
# curlでの詳細ログ確認
curl -v https://api.example.com結果: 200 OK(成功) この結果から、ネットワーク経路や相手サーバー自体には問題がなく、「Javaという言語環境」特有の問題であることが確定しました。
keytoolによるSSLデバッグ
Javaの標準ツール keytool を使い、JVMがどのようにハンドシェイクを試みているか詳細ログを出力します。
keytool -printcert -sslserver api.example.com:443 -J-Djavax.net.debug=ssl,handshakeこのログを確認したところ、相手サーバーが要求している暗号方式(Cipher Suite)に対し、Java側が適合するものを提示できていないことが分かりました。
3. 真犯人は「RSA鍵交換」の無効化
接続先のサーバー(特に金融系やレガシーな決済基盤)では、依然として以下の暗号スイートのみを許可しているケースがあります。
TLS_RSA_WITH_AES_256_GCM_SHA384TLS_RSA_WITH_AES_256_CBC_SHA256など
これらは 「RSA鍵交換」 を使用しますが、Java 17.0.18以降などの最新パッチでは、前方秘匿性(Forward Secrecy)がないことを理由に、デフォルトでこれらのアルゴリズムが制限、あるいは優先順位が極端に下げられるようになりました。
4. 解決策:ベースイメージのバージョン固定
今回のケースでは、Javaのバージョンを 17.0.18 から 17.0.17 へ一段階戻すことで、即座に疎通が復旧しました。
(Docker イメージのバージョン固定をした)
恒久的な対応案
将来的にJavaを最新版に維持しつつ、レガシーな接続先と通信を続けるには、以下の対応が必要になります。
- Javaのセキュリティポリシー(java.security)の変更
jdk.tls.disabledAlgorithmsプロパティから、接続に必用なアルゴリズム(RSA等)の制限を解除する。 - 接続先サーバーのモダナイズ依頼 接続先に対して、より安全な
ECDHE(楕円曲線ディフィー・ヘルマン)方式のサポートを依頼する。
5. まとめ
Javaのマイナーアップデートは、単なるバグ修正だけでなく、今回のような「デフォルトセキュリティ設定の変更」が含まれることがあります。
curlは通るのに Java だけ失敗する- 特定の古い暗号方式(RSA等)しか受け付けない接続先がある
これらの条件が揃った際は、Javaのパッチバージョンによるセキュリティポリシーの差異をまず疑ってみてください。
