OpenSSL Ciphers 에 대하여
이 글은 ssl cipher에 대한 이해가 부족하여 발생한 이슈 상황에 대하여 회고하는 글입니다.
상황
사내에서 nginx의 SSL 프로토콜 기준을 TLSv1.2 이상으로 지원해야 하는 일이 있었습니다. 모든 서비스에 공통으로 적용하도록 가이드가 내려왔기에, 이에 따라 기존 서비스들은 SSL 설정을 수정하였습니다.
가이드엔 SSL 프로토콜과 SSL Cipher 문자열의 수정이 있었는데요, 실질적으로 수정한 범위는 nginx의 ssl_protocols
와 ssl_ciphers
를 수정해주면 되었습니다. 이 중 ssl_ciphers
와 같은 경우 정보보호 강의 때 얼핏 들은 것으로 보이는 암호화 방식(AES, SHA…)으로 얽힌 복잡한 문자열 값을 설정해주어야 했었는데, 이때만 하더라도 그 문자열이 어떤 의미를 갖는지 정확히 알지 못하고 가이드만 따라 지정하였습니다.
그렇게 배포가 진행되었고 설정에 따라 nginx는 특정 cipher 방식에 대해서만 허용하도록 적용이 되었습니다. 이후로 해당 서버에는 요청이 인입되지 않는 이슈가 발생했었습니다...
원인
서버에 요청이 인입되지 않는 이유부터 거슬로 올라가면 L4 스위치에서 장비가 떼어져 있었습니다. 사내에선 지정한 health-check에 실패한 서버는 자동으로 L4 바인딩이 해제되기 때문에 요청이 인입되지 않았습니다. 그렇다면 왜 떨어져 나갔을까요?
문제는 서버(nginx)에서 허용하는 cipher 방식이 health-check 클라이언트에서 전달하는 cipher 방식을 허용하지 않고 있었기 때문입니다.
Cipher String 분석
아래의 코드는 nginx에 설정했던 Cipher 문자열입니다.
ssl_ciphers AES128:AES256:!COMPLEMENTOFDEFAULT:!EDH:!eNULL:!SHA1;
해당 문자열은 cipher string 혹은 cipher list라고 부르는데 허용할 cipher에 대한 정보를 명시하고 있습니다.
각 cipher는 콜론(:)으로 분리되며(콤마와 공백으로도 구분 가능하나 일반적으로 콜론을 사용) 명시하고싶은 cipher가 어떤 게 있는지 알고 싶다면 IANA TLS Cipher Suites Registry를 참고하면 됩니다.
콜론으로 끊어 보면 다음과 같은 cipher 항목들이 명시되고 있는데요, 찬찬히 살펴보겠습니다.
Cipher List Format 및 분석
몇가지 의미가 있는 prefix와 키워드가 존재합니다.
- prefix
!
: 해당 cipher에 대해서 절대 허용하지 않음. - prefix
-
: 해당 cipher를 리스트에서 제외한다. 하지만 이후 명시된다면 추가될 수 있다(제외 아님). - prefix
+
: 해당 cipher는 허용한다. +가 붙은 cipher는 리스트의 마지막으로 이동된다. -로 미허용 후 허용이 가능할것이다. aNULL
: 인증 기능이 없는 cipher suite. Man-In-The-Middle (MITM) 공격에 취약하다eNULL
,NULL
: 암호화하지 않는 cipher suite(null-encryption ciphers).HIGH
:"High" 수준의 cipher suites의 목록. OpenSSL 버전마다 포함되는 cipher suite가 다르며, 현재는 128 bit 보다 큰 키 길이를 가지는 cipher를 사용하는 cipher suite입니다.MEDIUM
:"Medium" 수준의 cipher suites의 목록. OpenSSL 버전마다 포함되는 cipher suite가 다르며, 현재는 128 bit 암호를 사용하는 cipher suite입니다.LOW
:"Low" 수준의 cipher suites의 목록. OpenSSL 버전마다 포함되는 cipher suite가 다르며, 현재는 64/56 bit 암호를 사용하는 cipher suite입니다. 하지만 Export 수준의 cipher suite들은 제외합니다.ALL
: eNULL을 제외한 모든 cipherCOMPLEMENTOFDEFAULT
: ALL의 부분집합 cipher이며 RC4와 익명 cipher들을 갖고있다.
더 많은 키워드들이 존재하며 링크에서 확인할 수 있습니다.
그렇다면 위에서 지정한 cipher string을 확인해보겠습니다.
AES128
,AES256
: 우선 128비트, 256비트 AES에 대하여 허용하고 있습니다!COMPLEMENTOFDEFAULT
:!
가 붙었기에 RC4와 익명 cipher 들에 대해선 제외(미허용)합니다!EDH
,!SHA1
: EDH, SHA1 방식도 제외됩니다.!eNULL
: 암호화하지 않는 cipher 들도 모두 제외됩니다.
허용 Cipher 알고리즘 확인
이렇게 설정했을 때 허용되는 cipher가 어떤 것인지 표를 보고 찾기는 어려울 것입니다.
$ openssl ciphers -v -s -tls1_2 -tls1_3 'AES128:AES256:!COMPLEMENTOFDEFAULT:!EDH:!eNULL:!SHA1'
openssl ciphers
명령을 통해 지원가능한 cipher 알고리즘을 확인할 수 있습니다.
사용한 옵션은 아래와 같습니다
-v
: verbose(자세히 출력)-s
: 지원되는 ciphers 만 출력-tls1_2
,-tls1_3
: TLSv1.2, TLS1.3 프로토콜이 지원되는 cipher(-s 옵션과 함께 사용)[cipherlist]
: 확인할 cipher list
$ openssl ciphers -v -s -tls1_2 -tls1_3 'AES128:AES256:!COMPLEMENTOFDEFAULT:!EDH:!eNULL:!SHA1'
ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
DH-DSS-AES128-GCM-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AESGCM(128) Mac=AEAD
DH-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AESGCM(128) Mac=AEAD
DH-RSA-AES128-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AES(128) Mac=SHA256
DH-DSS-AES128-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AES(128) Mac=SHA256
ECDH-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AESGCM(128) Mac=AEAD
ECDH-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AESGCM(128) Mac=AEAD
ECDH-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AES(128) Mac=SHA256
ECDH-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AES(128) Mac=SHA256
AES128-GCM-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(128) Mac=AEAD
AES128-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(128) Mac=SHA256
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(256) Mac=SHA384
ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA384
DH-DSS-AES256-GCM-SHA384 TLSv1.2 Kx=DH/DSS Au=DH Enc=AESGCM(256) Mac=AEAD
DH-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH/RSA Au=DH Enc=AESGCM(256) Mac=AEAD
DH-RSA-AES256-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AES(256) Mac=SHA256
DH-DSS-AES256-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AES(256) Mac=SHA256
ECDH-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AESGCM(256) Mac=AEAD
ECDH-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AESGCM(256) Mac=AEAD
ECDH-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AES(256) Mac=SHA384
ECDH-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AES(256) Mac=SHA384
AES256-GCM-SHA384 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(256) Mac=AEAD
AES256-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(256) Mac=SHA256
클라이언트에서 사용한 cipher는 TLSv1.2
, AES128-SHA
였는데요, 위의 결과에서 확인할 수 있듯이 서버 쪽에선 AES128-SHA
는 지원하지 않고 있어 health-check에 실패하게 되었습니다.
후기
사내 보안팀이 존재하고 공통적 가이드가 제공되기에 SSL관련 설정에 미숙하였는데요, 이슈가 있긴 했지만 이를 계기로 암호화 알고리즘이 어떻게 설정되는지 알 수 있었습니다. 가장 뿌듯한 점은 복잡했해서 눈에 잘 들어오지 않았던 cipher string도 더 이해할 수 있게 되었다는 점입니다.
앞으로 SSL의 변경 사항이 있을 시, access.log를 통해 인입되는 ssl_cipher 종류를 파악, 수정할 서버에서 해당 cipher들에 대하여 모두 커버되는지 확인하여 이슈를 미연에 방지할 수 있을 것입니다.