상황
신규 이전한 서버에서 애플리케이션을 띄우는데 다음과 같은 에러를 마주하게됐다. 이전 서버에서와 달라진 점은 CentOS 7.4에서 7.9로 바뀐 것밖에 없는데 뭐가 문제일까 이해가 잘되지 않았다. 또한 로컬 환경에서는 문제없이 실행되는 상황이라 추적이 더 어려웠다.
1
2
3
4
5
6
Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
at sun.security.ssl.HandshakeContext.<init>(HandshakeContext.java:171)
at sun.security.ssl.ClientHandshakeContext.<init>(ClientHandshakeContext.java:103)
at sun.security.ssl.TransportContext.kickstart(TransportContext.java:220)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:433)
at com.mysql.jdbc.ExportControlled.transformSocketToSSLSocket(ExportControlled.java:186)
- 에러 로그를 봤을때 SSL 핸드셰이크하는 과정에서 적합한 프로토콜 또는 Cipher Suite가 없어서 발생한 에러로 보인다. 에러가 발생한 부분의 코드를 살펴보자
sun.security.ssl.HandshakeContext#HandshakeContext- 결과적으로
activeProtocols가 빈 리스트라 발생한 에러임을 알 수 있다. - 로컬 환경에서 디버거로 추적해보았을 때,
getActiveProtocols메서드의 첫 번째 파라미터인enabledProtocols에 세팅된 프로토콜은TLS1.0,TLS1.1이었다. - 원인 파악을 위해서는 다음 두 가지 살펴봐야 할 것 같다.
enabledProtocols엔 어떻게TLS1.0,TLS1.1이 세팅된 것일까- 왜 기존 서버와 로컬에서는 되고 신규 서버에서는 안될까 ?
그럼, 먼저 전반적인 호출 흐름을 살펴보자
호출 흐름 살펴보기
- 에러 로그 콜 스택에서도 볼 수 있듯이, 객체 호출 흐름은 다음과 같다.
1 2 3 4 5
com.mysql.jdbc.ExportControlled.transformSocketToSSLSocket -> sun.security.ssl.SSLSocketImpl.startHandshake -> sun.security.ssl.TransportContext.kickstart -> sun.security.ssl.ClientHandshakeContext.<init> -> sun.security.ssl.HandshakeContext.<init>
- 두 번째 단계인
SSLSocketImpl의 클래스를 보면 아래와 같이SSLContextImpl,TransportContext클래스를 인스턴스 변수로 선언하고 있고final로 되어있는 것을 알 수 있다. 즉,SSLSocketImpl이 생성될 때,SSLContextImpl,TransportContext객체도 반드시 함께 생성되는 것을 알 수 있다.1 2 3 4 5 6 7
public final class SSLSocketImpl extends BaseSSLSocketImpl implements SSLTransport { final SSLContextImpl sslContext; final TransportContext conContext; ... }
TransportContext.kickstart이후의 흐름은ClientHandshakeContext객체를 생성하는 것인데, 맨 처음sun.security.ssl.HandshakeContext#HandshakeContext을 다시 살펴보면 생성자 파라미터로SSLContextImpl과TransportContext가 필요한 것을 알 수 있다.- 다시 거꾸로 정리하면,
ClientHandshakeContext는 생성시HandshakeContext생성자를 호출하고,HandshakeContext를 생성하기 위해서는SSLSocketImpl의SSLContextImpl과TransportContext가 반드시 필요하다. 그리고, 이 두 객체는SSLSocketImpl이 생성될 때 함께 세팅된다. - 따라서,
ExportControlled.transformSocketToSSLSocket에서 어떻게SSLSocketImpl를 생성하는지, 그리고 여기에 세팅되는SSLContextImpl과TransportContext중 특히 (getActiveProtocols의 파라미터가 되는)TransportContext의SSLConfiguration이 어떤지가 중요할 것 같다.
이제 위 흐름을 생각하면서 원인 파악을 위한 두 가지 부분을 체크해보자.
1. enabledProtocols엔 어떻게 TLS1.0, TLS1.1이 세팅된 것일까 ?
ExportControlled.transformSocketToSSLSocket에서는SocketFactory를 통해SSLSocket을 생성한다. (mysqlIO.mysqlconnection = sslFact.connect)- 그리고 맨 아래
((SSLSocket) mysqlIO.mysqlConnection).setEnabledProtocols(allowedProtocols.toArray(new String[0]))부분을 주목해서 보자. - 에러가 발생한 애플리케이션의 DB 서버는 MySQL 5.7버전을 사용하고 있기 때문에, 따로
enabledTLSProtocols옵션을 주지 않은 이상tryProtocols = new String[] { TLSv1_1, TLSv1 };이 부분을 탈 것이고,configuredProtocols에 이 두 개의 프로토콜이 세팅된다. - 결과적으로,
if (jvmSupportedProtocols.contains(protocol) && configuredProtocols.contains(protocol))조건을 만족하는 프로토콜은TLS1.1,TLS1.0밖에 없기때문에enabledTLSProtocols에 두 개의 프로토콜이 세팅된다.
2. 왜 로컬에서는 되고 서버에서는 안될까 ?
HandshakeContext의getActiveProtocols메서드에서algorithmConstraints.permits부분을 자세히 살펴보았다.sun.security.ssl.SSLAlgorithmConstraints#permits의tlsDisabledAlgConstraints.permits부분을 타는 것을 알 수 있었다.sun.security.util.DisabledAlgorithmConstraints#permits- 요약하자면,
checkAlgorithm에서 통과하지 못한 TLS 프로토콜은 available하지 않는 프로토콜이며, 이는 해당 프로토콜이disabledAlgorithms목록에 있는지로 판단한다.1 2 3 4 5 6 7 8 9 10 11 12 13
@Override public final boolean permits(Set<CryptoPrimitive> primitives, String algorithm, AlgorithmParameters parameters) { if (!checkAlgorithm(disabledAlgorithms, algorithm, decomposer)) { return false; } if (parameters != null) { return algorithmConstraints.permits(algorithm, parameters); } return true; }
- 요약하자면,
disabledAlgorithms은DisabledAlgorithmConstraints가 생성될때 세팅된다.PROPERTY_TLS_DISABLED_ALGS은"jdk.tls.disabledAlgorithms"로 선언되어있다.1 2 3 4 5
final class SSLAlgorithmConstraints implements AlgorithmConstraints { private static final AlgorithmConstraints tlsDisabledAlgConstraints = new DisabledAlgorithmConstraints(PROPERTY_TLS_DISABLED_ALGS, new SSLAlgorithmDecomposer());
1 2 3 4
public DisabledAlgorithmConstraints(String propertyName, AlgorithmDecomposer decomposer) { super(decomposer); disabledAlgorithms = getAlgorithms(propertyName); // propertyName = "jdk.tls.disabledAlgorithms"
getAlgorithms은$JAVA_HOME/jre/lib/security/java.security파일에서propertyName인jdk.tls.disabledAlgorithms에 정의된 알고리즘(프토토콜) 목록을 가져온다.- 로컬에서 확인해보니 해당 프로퍼티는 다음과 같이 선언되어 있었다. (
openjdk-1.8.0.242버전)1 2
jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \ EC keySize < 224, 3DES_EDE_CBC, anon, NULL
- 여기서 문득, 신규 서버의 자바 릴리즈 버전은 뭐지? 라는 생각이 들었고, 확인해보니 기존에 사용하던
openjdk-1.8.0.242이 아닌openjdk8u362-b09버전이 사용되고 있었다. - 해당 버전을 로컬에 설치해서 살펴본 결과,
jdk.tls.disabledAlgorithms프로퍼티는 다음과 같이 선언되어 있었다. (TLSv1, TLSv1.1이 추가되어있다)1 2 3
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \ DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \ include jdk.disabled.namedCurves
- 이로써 기나긴 삽질의 시간을 끝낼 수 있게 되었다..!
정리
- MySQL 드라이버(mysql-connector-java 5.1.46 버전 기준)에서
enabledTLSProtocols과 같은 세팅이 별도로 정의되어있지 않으면, MySQL 5.7 서버와 SSL 핸드셰이크를 위한 프로토콜로TLS1.0,TLS.1.1이 사용된다. - 같은 자바 버전이더라도 릴리즈 버전에 따라 SSL 통신시 disable된 프로토콜이 다를 수 있다. 이로 인해, SSL 핸드셰이크가 제대로 이루어지지 않을 수 있다.
(이전과 달라진게 없는데 왜 문제가 생기지..? 라고 섣불리 생각하지 말자)
생각해본 조치 방안
- MySQL 버전 업그레이드
enabledTLSProtocols정의jdbc:mysql://{ip}:{port}/{dbName}?&enabledTLSProtocols={TLS_VERSION}- 해당 MySQL 서버에서 지원하는 TLS 프로토콜이어야한다.
- 지원하는 프로토콜 확인 방법 :
show variables like 'tls_%';
- 지원하는 프로토콜 확인 방법 :
useSSL=false(해당 옵션을 주면 SSL 통신 자체를 하지 않는다.)jdbc:mysql://{ip}:{port}/{dbName}?&useSSL=falsecom.mysql.jdbc.MysqlIO#doHandshake1 2 3 4
if (!this.connection.getUseSSL()) { ... } else { negotiateSSLConnection(user, password, database, packLength);