Java

Java 16にアップグレードするとMySQLへの接続や、JavaMailによるメール送信で javax.net.ssl.SSLHandshakeExceptionが発生する

3秒で要約: Java 16ではTLSv1.0が無効化されている。TLSv1.2で接続しましょう

現象と対策 – MySQL

Java 16にアップデートしたところ、ステージング環境で以下のような例外が発生しました。

Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
	at java.base/sun.security.ssl.HandshakeContext.(HandshakeContext.java:172)
	at java.base/sun.security.ssl.ClientHandshakeContext.(ClientHandshakeContext.java:98)
	at java.base/sun.security.ssl.TransportContext.kickstart(TransportContext.java:238)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:434)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:412)
	at com.mysql.cj.protocol.ExportControlled.performTlsHandshake(ExportControlled.java:336)
	at com.mysql.cj.protocol.StandardSocketFactory.performTlsHandshake(StandardSocketFactory.java:188)
	at com.mysql.cj.protocol.a.NativeSocketConnection.performTlsHandshake(NativeSocketConnection.java:99)
	at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:325)

使っているドライバは mysql:mysql-connector-java:8.0.23 です。
調べたところこれは接続先ホストからJavaが対応していないTLS(いわゆるSSL)のプロトコルバージョンを要求されたときに出るものみたいです。

接続先のMySQL on Amazon RDSではMySQL 5.6.44なのですが、TLS1.0までしかサポートしていないようです。
MySQL on Amazon RDS > Using SSL with a MySQL DB instance

MySQL 5.6はすでに2021年2月5日にEOLで、AWSでも2021年8月3日にサポート終了予定です。
Amazon RDS for MySQL バージョン 5.6 のサポート終了のお知らせ
最新のバージョン8への移行が推奨されていますが、5.6から8.0へは一気にアップグレードできないので5.7を経て8.0にアップグレードしたところ症状が治まりました。(深掘りしたSQLは発行していないので、コードの変更はなしにテストケースは全て通りました。)

現象と対策 – JavaMail

JavaMailでメール送信する箇所でも似た例外が出ていました。

Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
	at java.base/sun.security.ssl.HandshakeContext.(HandshakeContext.java:172)
	at java.base/sun.security.ssl.ClientHandshakeContext.(ClientHandshakeContext.java:98)
	at java.base/sun.security.ssl.TransportContext.kickstart(TransportContext.java:238)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:434)
	at java.base/sun.security.ssl.SSLSocketImpl.ensureNegotiated(SSLSocketImpl.java:903)
	at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:994)
	at com.sun.mail.util.TraceInputStream.read(TraceInputStream.java:110)

最新のcom.sun.mail:javax.mail:1.6.2では問題ないのですが、org.eclipse.jetty.aggregate:jetty-all:9.4.38.v20210224が依存しているorg.eclipse.jetty.orbit:javax.mail.glassfish:1.4.1.v201005082020が優先されてしまっていたようです。これをexcludeすることで回避できました。

根本原因

Java 15で接続できていたのにJava 16で接続できなくなったのはセキュリティの設定が変更になったためです。

$JAVA_HOME/conf/security/java.security を見ると以下のようにTLSv1、TLSv1.1が無効化されている記述が見つかります。

# Note: This property is currently used by the JDK Reference implementation.
# It is not guaranteed to be examined and used by other implementations.
#
# Example:
#   jdk.tls.disabledAlgorithms=MD5, SSLv3, DSA, RSA keySize < 2048, \
#       rsa_pkcs1_sha1, secp224r1
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \
    DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL

Java 15.0.2ではこうなっています。

# Note: This property is currently used by the JDK Reference implementation.
# It is not guaranteed to be examined and used by other implementations.
#
# Example:
#   jdk.tls.disabledAlgorithms=MD5, SSLv3, DSA, RSA keySize < 2048, \
#       rsa_pkcs1_sha1, secp224r1
jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \
    EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \
    include jdk.disabled.namedCurves

ちなみにTLSv1.0,1.1はIETFで非推奨とされJava 8,11,15でも無効化されるようです。

Quick & Dirty消極的回避策

jdk.tls.disabledAlgorithmsから TLSv1 を削除することでも回避出来ます。
が、そもそも非推奨になっているプロトコルなのでTLSv1.2で接続したり、グローバルの回線を通らないようにしてプレイン通信するようにすべきです。

まとめ

CIではMySQLへの接続はTLSを使っておらず、メール送信も実際には行わない仕組みにしているので検出できませんでした。
本番DBと同じ構成のステージングサーバで発生したので障害には至りませんでした。日頃からCIや限りなく本番に近い構成のステージングサーバでの検証をしておきましょう。