Posts javax.net.ssl.SSLHandshakeException - No appropriate protocol (protocol is disabled or cipher suites are inappropriate)의 원인을 찾아서
Post
Cancel

javax.net.ssl.SSLHandshakeException - No appropriate protocol (protocol is disabled or cipher suites are inappropriate)의 원인을 찾아서

상황

신규 이전한 서버에서 애플리케이션을 띄우는데 다음과 같은 에러를 마주하게됐다. 이전 서버에서와 달라진 점은 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 image

  • 결과적으로 activeProtocols가 빈 리스트라 발생한 에러임을 알 수 있다.
  • 로컬 환경에서 디버거로 추적해보았을 때, getActiveProtocols 메서드의 첫 번째 파라미터인 enabledProtocols에 세팅된 프로토콜은 TLS1.0, TLS1.1이었다.
  • 원인 파악을 위해서는 다음 두 가지 살펴봐야 할 것 같다.
    1. enabledProtocols엔 어떻게 TLS1.0, TLS1.1이 세팅된 것일까
    2. 왜 기존 서버와 로컬에서는 되고 신규 서버에서는 안될까 ?

그럼, 먼저 전반적인 호출 흐름을 살펴보자

호출 흐름 살펴보기

  • 에러 로그 콜 스택에서도 볼 수 있듯이, 객체 호출 흐름은 다음과 같다.
    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을 다시 살펴보면 생성자 파라미터로 SSLContextImplTransportContext가 필요한 것을 알 수 있다.
  • 다시 거꾸로 정리하면, ClientHandshakeContext는 생성시 HandshakeContext 생성자를 호출하고, HandshakeContext를 생성하기 위해서는 SSLSocketImplSSLContextImplTransportContext가 반드시 필요하다. 그리고, 이 두 객체는 SSLSocketImpl이 생성될 때 함께 세팅된다.
  • 따라서, ExportControlled.transformSocketToSSLSocket에서 어떻게 SSLSocketImpl를 생성하는지, 그리고 여기에 세팅되는 SSLContextImplTransportContext중 특히 (getActiveProtocols의 파라미터가 되는) TransportContextSSLConfiguration이 어떤지가 중요할 것 같다.

이제 위 흐름을 생각하면서 원인 파악을 위한 두 가지 부분을 체크해보자.

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에 두 개의 프로토콜이 세팅된다.

image

2. 왜 로컬에서는 되고 서버에서는 안될까 ?

  • HandshakeContextgetActiveProtocols 메서드에서 algorithmConstraints.permits 부분을 자세히 살펴보았다. image

  • sun.security.ssl.SSLAlgorithmConstraints#permitstlsDisabledAlgConstraints.permits 부분을 타는 것을 알 수 있었다. image

  • 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;
      }
      
  • disabledAlgorithmsDisabledAlgorithmConstraints가 생성될때 세팅된다. 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 파일에서 propertyNamejdk.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이 사용된다.
  • 같은 자바 버전이더라도 릴리즈 버전에 따라 disable된 프로토콜이 다를 수 있다.
    (이전과 달라진게 없는데 왜 문제가 생기지..? 라고 섣불리 생각하지 말자)

가능한 조치 방안

  • 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=false
    • com.mysql.jdbc.MysqlIO#doHandshake
      1
      2
      3
      4
      
          if (!this.connection.getUseSSL()) {
              ...
          } else {
              negotiateSSLConnection(user, password, database, packLength);
      
This post is licensed under CC BY 4.0 by the author.

wait_timeout을 초과한 커넥션을 사용해서 겪은 이슈 (The last packet successfully received from the server was ... milliseconds ago)

JDBC / MyBatis / MyBatis-Spring 훑어보기