수정 약 39분

ERR_SSL_VERSION_OR_CIPHER_MISMATCH와 ERR_SSL_PROTOCOL_ERROR 해결

이 글의 순서 11개 절
  1. 판별 순서
  2. s_client 출력 읽기
  3. 원인 1. 서버가 TLS 1.0/1.1만 지원한다
  4. 원인 2. 구형 암호 스위트만 남았다
  5. 원인 3. 요청한 이름(SNI)에 줄 인증서가 없다
  6. 원인 4. CDN 엣지 인증서가 이 이름을 덮지 않는다 (Cloudflare)
  7. 원인 5. 오래된 OS와 라이브러리의 기본값
  8. 원인 6. 443에서 TLS가 아닌 것이 답한다
  9. Chrome이 에러 코드를 고르는 방식
  10. certdesk 진단에서는
  11. 참조

ERR_SSL_VERSION_OR_CIPHER_MISMATCH(-113)와 ERR_SSL_PROTOCOL_ERROR(-107)는 인증서 에러가 아니다. ERR_CERT_* 계열은 서버가 보낸 인증서를 검증하다 실패한 것이고, 이 둘은 그보다 앞에서 끊긴 것이다. TLS 버전과 암호 스위트를 맞추다가, 또는 상대가 보낸 바이트를 TLS로 읽다가 끝난다. 서버가 인증서를 보내기도 전인 경우가 많다.

Chrome은 여러 종류의 TLS 경고(alert)와 내부 오류를 이 두 코드로 접어 넣는다. 그래서 같은 화면 뒤에 전혀 다른 원인이 숨는다. 인증서 자체가 문제라면 이름 불일치, 신뢰할 수 없는 발급자, 유효기간 글을 본다.

판별 순서

첫 판별은 openssl s_client 한 번이면 된다. 마지막 줄에 찍힌 이유 문자열과 경고 번호를 보고 이 순서로 좁힌다.

1. openssl s_client -connect H:443 -servername H -brief
   └ wrong version number          → 443 에 TLS 가 아님 (원인 6)
   └ unsupported protocol          → 2번으로
   └ SSL alert number 40           → 3번으로
   └ SSL alert number 112          → SNI 거절, server_name 확인 (원인 3)
   └ SSL alert number 80           → 서버 에러 로그 확인 (원인 3)
   └ CONNECTION ESTABLISHED        → 4번으로

2. -tls1 (또는 -tls1_1) -cipher 'DEFAULT@SECLEVEL=0'
   └ 붙음                          → TLS 1.0/1.1 전용. 설정 또는 OS·OpenSSL (원인 1·5)

3. 이름(SNI)을 바꿔 가며 비교
   └ 이 이름만 실패                → 인증서 없음·CDN 범위 (원인 3·4)
   └ 모든 이름이 실패              → nmap·testssl.sh 로 스위트 목록 (원인 2)

4. 서버는 붙는데 브라우저만 실패
   └ -tls1_2 + intermediate 스위트로 실패 → 스위트 정리 (원인 2)
   └ 특정 망에서만 실패            → 중간 장비 (원인 6)
   └ 붙은 직후 alert 116           → 클라이언트 인증서 요구 (원인 6)

원인 1·2·5·6은 인증서를 새로 받아도 그대로다. 버전과 스위트 설정, 443에서 무엇이 듣고 있는지를 고쳐야 한다. 원인 3·4는 인증서가 필요하지만, 고칠 곳은 그 인증서가 요청한 이름으로 실제로 나가게 하는 연결 설정이다.

s_client 출력 읽기

openssl s_client -connect example.com:443 -servername example.com -brief </dev/null

정상이면 이렇게 끝난다(2026-10-04, OpenSSL 3.6.5로 badssl.com에 실행).

CONNECTION ESTABLISHED
Protocol version: TLSv1.2
Ciphersuite: ECDHE-RSA-AES128-GCM-SHA256
Peer certificate: CN=*.badssl.com
Verification: OK

실패하면 error: 줄의 이유 문자열과 SSL alert number를 본다. 표의 출력은 모두 실제로 받은 것이다.

출력 뜻 Chrome 원인
unsupported protocol 서버가 고른 버전이 클라이언트 범위 밖 -113 1·5
alert handshake failure ... SSL alert number 40 서버가 ClientHello를 받고 거절 -113 2·3·4
tlsv1 unrecognized name ... SSL alert number 112 서버가 SNI 이름을 거절 -159 3
tlsv1 alert internal error ... SSL alert number 80 서버 내부 오류 (인증서 로드 실패 등) -107 3
wrong version number 돌아온 바이트가 TLS 레코드가 아님 -107 6
no protocols available 내 OpenSSL이 요청한 버전을 쓸 수 없음 해당 없음 아래 참고

-connect와 -servername에는 주소창의 이름을 그대로 넣는다. SNI가 달라지면 서버가 다른 설정으로 응답할 수 있다(원인 3·4).

버전을 하나씩 강제할 때

openssl s_client -connect example.com:443 -servername example.com -tls1_3 -brief </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -brief </dev/null

둘 다 실패하는데 서버는 살아 있다면 구버전을 시험한다. 여기서 로컬 OpenSSL이 걸림돌이 된다.

$ openssl s_client -connect tls-v1-0.badssl.com:1010 -servername tls-v1-0.badssl.com -tls1 -brief </dev/null
...error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available:...

OpenSSL 3.0부터 SHA1·MD5 기반 서명의 보안 강도를 낮춰 잡았다. 그래서 SSL 3, TLS 1.0, TLS 1.1은 기본 보안 수준(1)에서 동작하지 않는다(마이그레이션 가이드). 보안 수준 0을 같이 준다.

$ openssl s_client -connect tls-v1-0.badssl.com:1010 -servername tls-v1-0.badssl.com \
    -tls1 -cipher 'DEFAULT@SECLEVEL=0' -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1
Ciphersuite: ECDHE-RSA-AES256-SHA
Hash used: MD5-SHA1

Homebrew의 OpenSSL 3.6.5와 4.0.3 모두 이렇게 붙었다. macOS 기본 /usr/bin/openssl(LibreSSL 3.3.6)은 -tls1만으로 붙었다. no protocols available은 내 쪽 사정이니 서버를 판정하는 데 쓰지 않는다.

curl의 --tlsv1.2는 최소 버전이다

curl 매뉴얼은 --tlsv1.2를 “TLS 1.2 or later”로 설명한다. 예전 매뉴얼은 1.2만 쓰는 옵션처럼 적었는데, TLS 라이브러리마다 동작이 달라서 바뀌었다. 상한은 --tls-max로 건다.

curl -sv -o /dev/null --tlsv1.2 --tls-max 1.2 https://example.com/   # 정확히 1.2
curl -sv -o /dev/null --tlsv1.3 https://example.com/                 # 1.3

-v 출력의 SSL connection using 줄에 협상된 버전과 스위트가 나온다.

서버가 받는 스위트 목록

s_client는 내 클라이언트가 아는 스위트로만 시험한다. 서버가 받는 목록 전체가 필요하면 nmap이나 testssl.sh 같은 스캐너를 쓴다. 둘 다 연결을 여러 번 반복하니 자기 서버나 허락받은 대상에만 돌린다.

nmap --script ssl-enum-ciphers -p 443 example.com

nmap 문서에 따르면 이 스크립트는 스위트와 압축 방식을 하나씩 바꿔 가며 연결을 시도하고, 받아들인 스위트를 A~F 등급과 함께 보여 준다. 아래는 nmap 7.99로 rc4.badssl.com을 본 결과 중 TLSv1.0 부분이다. TLSv1.1과 1.2도 같은 두 스위트뿐이었다.

| ssl-enum-ciphers:
|   TLSv1.0:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - C
|       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - C
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       Broken cipher RC4 is deprecated by RFC 7465

testssl.sh는 -p(--protocols)로 프로토콜만 빠르게 본다. 3.2 판(Docker 이미지)으로 tls-v1-0.badssl.com:1010을 본 결과다.

 SSLv2      not offered (OK)
 SSLv3      not offered (OK)
 TLS 1      offered (deprecated)
 TLS 1.1    not offered
 TLS 1.2    not offered and downgraded to a weaker protocol
 TLS 1.3    not offered and downgraded to a weaker protocol

출력 첫머리에 Testing protocols via sockets라고 나오듯 프로토콜 검사를 자체 소켓 코드로 한다. 그래서 로컬 OpenSSL의 보안 수준에 덜 묶인다.

원인 1. 서버가 TLS 1.0/1.1만 지원한다

서버가 TLS 1.2 이상을 모르면 최신 브라우저는 붙지 않는다. 막은 시점은 브라우저마다 다르다.

대상 시점 내용
Chrome 72 폐지 예고, DevTools 경고
Chrome 79 해당 사이트에 ‘Not Secure’ 표시
Chrome 84 전체 화면 경고 (우회 가능)
Chrome 98 경고 우회 불가
Edge 84 기본 비활성. SSLVersionMin으로 TLS 1.0·1.1을 다시 켜는 값은 91부터 지원 안 함(정책 자체는 98에서 제거)
Firefox 78 (2020-06-30) 기본 비활성. 처음 계획은 74였다
Safari 13.1 (WebKit 공지 2020-04-03) 리소스 하나라도 TLS 1.0/1.1이면 ‘Not Secure’
Apple OS iOS 15·macOS 12 deprecated. “support will be removed in future releases” (2021-09-21 공지)
Java 8u291·11.0.11 기본 비활성 (JDK-8202343)

Safari는 사정이 조금 다르다. WebKit이 2018-10-15에 “2020년 3월부터 제거”를 예고했지만, 이후 Apple 문서는 TLS 1.1 이하 사이트에 ’Not Secure’를 표시한다고 설명하고 플랫폼 보안 가이드도 TLS 1.0~1.3 지원을 적는다. 공식 문서만 보면 Safari는 아직 막기보다 경고하는 쪽이다.

Chrome과 Edge는 -113을 낸다. Firefox는 SSL_ERROR_UNSUPPORTED_VERSION 류의 코드와 TLS 1.2 안내 문구를 보여 주고, Java는 The server selected protocol version ... is not accepted by client preferences ...를 남긴다. 업데이트가 멈춘 구형 클라이언트는 그대로 붙으니 “일부 사용자만 된다”는 제보로 들어오기도 한다.

옵션 없이 붙으면 unsupported protocol이 나오고, -tls1 -cipher 'DEFAULT@SECLEVEL=0'(또는 -tls1_1)로만 붙으면 확정이다.

$ openssl s_client -connect tls-v1-0.badssl.com:1010 -servername tls-v1-0.badssl.com -brief </dev/null
...error:0A000102:SSL routines:ssl_choose_client_version:unsupported protocol:...

nginx는 ssl_protocols로 정한다.

ssl_protocols TLSv1.2 TLSv1.3;

지금 nginx는 기본값이 이미 TLSv1.2 TLSv1.3이다. TLSv1.3은 1.23.4(2023-03-28)부터 기본이 됐고, TLSv1과 TLSv1.1은 1.27.3(2024-11-26)에서 기본값에서 빠졌다. 그런데도 TLS 1.0만 나오는 nginx라면 설정에서 TLSv1.2가 빠졌거나 오래된 OpenSSL과 묶인 경우다. nginx 문서에 따르면 TLSv1.1·TLSv1.2는 OpenSSL 1.0.1 이상, TLSv1.3은 OpenSSL 1.1.1 이상에서만 동작한다. 빌드에 쓴 OpenSSL은 nginx -V로 본다.

nginx -V 2>&1 | grep -i openssl
# built with OpenSSL 3.5.7 9 Jun 2026 ...

ssl_protocols를 server 블록에 써도 default server의 값이 쓰일 수 있다(nginx 문서). 같은 IP:443의 default server에 넣는다.

Apache는 SSLProtocol로 정한다.

SSLProtocol -all +TLSv1.2 +TLSv1.3

기본값 all -SSLv3의 all은 OpenSSL 1.0.1 이상에서 TLSv1~TLSv1.2를, 1.1.1 이상에서 TLSv1.3까지 포함한다. OpenSSL 1.1.1 미만이거나 Apache 2.4.42 미만이면 이름 기반 가상 호스트의 SSLProtocol이 따로 적용되지 않고, 그 IP:port에 처음 선언된 가상 호스트(base virtual host)의 값을 따른다.

서버별 인증서와 TLS 설정 위치는 Nginx 가이드와 Apache 가이드에 있다. 바꾼 뒤에는 -tls1_2는 붙고 -tls1 -cipher 'DEFAULT@SECLEVEL=0'은 실패하는지 다시 본다.

원인 2. 구형 암호 스위트만 남았다

TLS 1.2로 말은 하는데 스위트가 RC4나 3DES뿐인 서버다. 버전이 맞아도 고를 스위트가 없으면 서버는 ClientHello에 alert 40으로 답하고, Chrome은 -113을 낸다.

스위트 근거
RC4 RFC 7465 (2015): “TLS clients MUST NOT include RC4 cipher suites in the ClientHello message.” Chrome의 TLS 라이브러리인 BoringSSL의 스위트 표(ssl_cipher.cc)에는 RC4가 아예 없다. Apple은 iOS 10·macOS 10.12에서 RC4를 deprecated로 바꿨다
3DES Chrome 93에서 TLS_RSA_WITH_3DES_EDE_CBC_SHA 제거. 이유는 Sweet32·Lucky Thirteen
export·MD5 MAC OpenSSL 보안 수준 1이 export 스위트 전부와 MD5 MAC 스위트를 금지한다

Java도 같은 방향이다. OpenJDK 최신 소스의 기본 java.security는 jdk.tls.disabledAlgorithms에 RC4, DES, 3DES_EDE_CBC, anon, NULL을 넣고 TLS_RSA_*까지 더해 두었다. 이 기본값을 쓰는 JDK 클라이언트에서는 RSA 키 교환 스위트만 여는 서버가 먼저 깨진다.

2026-10-04에 OpenSSL 3.6.5로 rc4.badssl.com과 3des.badssl.com에 붙었더니 둘 다 이렇게 끝났다.

...error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:...:SSL alert number 40

-cipher 'ALL@SECLEVEL=0'을 줘도 같았다. 이 빌드의 openssl ciphers -v 'ALL@SECLEVEL=0' 목록에는 RC4도 3DES도 없다. 그러니 s_client로는 “공통 스위트 없음”까지만 알 수 있고, 서버가 받는 목록은 앞의 nmap이나 testssl.sh로 봐야 한다.

TLS 1.2로 붙기는 하는 서버라면 Mozilla intermediate의 TLS 1.2 스위트(아래 표)만 허용하고 붙어 본다.

openssl s_client -connect example.com:443 -servername example.com -tls1_2 \
  -cipher 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305' \
  -brief </dev/null

badssl.com은 ECDHE-RSA-AES128-GCM-SHA256으로 붙었고 rc4.badssl.com은 alert 40으로 끝났다. 여기서 실패하면 서버의 TLS 1.2 스위트가 현대 기준 밖에 있다.

스위트 목록은 손으로 짜지 말고 Mozilla Server Side TLS 가이드라인을 따른다. 2026-10-04 기준 가이드라인 6.0의 intermediate 프로필은 이렇다.

항목 값
프로토콜 TLSv1.2, TLSv1.3
TLS 1.2 스위트 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305
TLS 1.3 스위트 TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256
서버 우선 순서 쓰지 않음
가장 오래된 지원 클라이언트 Firefox 31.3.0, Android 4.4.2, Chrome 49, Edge 15 (Windows 10), IE 11 (Windows 10), Java 8u161, OpenSSL 1.0.1l, Opera 20, Safari 9

TLS 1.3만 쓰는 modern 프로필도 있다. 5.7 판에 있던 TLS 1.0~1.3용 old 프로필은 6.0 판에서 빠졌다. 구형 클라이언트를 위해 기댈 공식 “old” 설정은 이제 없다.

nginx에서는 이렇게 둔다.

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

nginx의 ssl_ciphers 기본값은 HIGH:!aNULL:!MD5다. 고치기 전에 nginx -T | grep -n ssl_ciphers로 실제 적용값부터 찾는다. 줄이 없으면 기본값이 쓰이고 있는 것이다.

Apache에서는 이렇다.

SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder off

Apache의 SSLCipherSuite 기본값은 DEFAULT(OpenSSL 버전에 따라 다름)다. TLS 1.3 스위트는 이 줄과 별개로 SSLCipherSuite TLSv1.3 ... 형식으로 지정한다. Mozilla SSL Configuration Generator(ssl-config.mozilla.org)가 같은 가이드라인으로 서버별 설정 조각을 만들어 준다.

원인 3. 요청한 이름(SNI)에 줄 인증서가 없다

서버는 ClientHello의 SNI를 보고 인증서를 고른다. 그 이름에 줄 인증서가 없을 때 서버가 어떻게 반응하느냐에 따라 Chrome의 에러가 달라진다. 2026-10-04에 nginx 1.31.6(Docker 공식 이미지)과 openssl s_server로 재현한 결과다.

서버 상태 클라이언트가 받는 것 Chrome
인증서 없는 TLS 리스너 (openssl s_server -nocert) alert 40 -113
nginx 기본 server에 ssl_reject_handshake on alert 112 unrecognized name -159
nginx ssl_certificate에 변수 경로를 썼는데 그 이름의 파일이 없음 alert 80 internal error -107
nginx listen ... ssl server에 인증서가 없음 기동·리로드 자체가 실패 해당 없음
다른 server 블록의 인증서로 대신 응답 핸드셰이크 성공, 이름 불일치 ERR_CERT_COMMON_NAME_INVALID

인증서가 없다는 같은 상황이 Chrome에서는 세 가지 코드로 갈린다.

nginx는 인증서 없는 TLS server를 아예 띄우지 않는다. listen 443 ssl인데 ssl_certificate가 없는 server 블록이 있으면 기동이 실패했다. nginx -t에서 잡힌다.

nginx: [emerg] no "ssl_certificate" is defined for the "listen ... ssl" directive in <설정 파일>:<줄>

변수 경로는 런타임에 터진다. nginx 1.15.9부터 ssl_certificate $ssl_server_name.crt;처럼 파일 이름에 변수를 쓸 수 있고, 이때는 인증서를 핸드셰이크마다 읽는다(nginx 문서). 새 이름의 파일을 깔지 않았으면 그 이름의 연결만 alert 80으로 끊기고, 에러 로그에 이렇게 남는다.

cannot load certificate "<경로>": BIO_new_file() failed (... No such file or directory ...) while SSL handshaking

Chrome에서는 -107이다. 인증서 문제가 “프로토콜 에러”로 보이는 경우다.

ssl_reject_handshake는 일부러 거절하는 설정이다. 1.19.4부터 있고, SNI가 어떤 server_name과도 안 맞거나 SNI가 없으면 alert 112로 거절한다. Chrome에서 ERR_SSL_UNRECOGNIZED_NAME_ALERT가 보이면 새 도메인의 server_name 누락부터 의심한다. 이 설정과 default server 구성은 ERR_CERT_COMMON_NAME_INVALID 원인별 해결의 기본 인증서 항목에 있다.

판별은 이름마다 SNI를 바꿔 붙어 보면 된다.

openssl s_client -connect example.com:443 -servername www.example.com -brief </dev/null
openssl s_client -connect example.com:443 -servername new.example.com -brief </dev/null
openssl s_client -connect example.com:443 -noservername -brief </dev/null

같은 IP에서 한 이름만 실패하면 인증서나 server 블록 문제다. 모든 이름이 실패하면 원인 1이나 2 쪽이다.

원인 4. CDN 엣지 인증서가 이 이름을 덮지 않는다 (Cloudflare)

CDN을 쓰면 브라우저가 TLS를 맺는 상대는 원서버가 아니라 엣지다. 엣지에 그 이름의 인증서가 없으면 원서버가 멀쩡해도 핸드셰이크에서 끝난다.

2026-10-04에 Cloudflare 엣지 IP로 붙으면서 엣지 인증서가 없는 이름을 SNI로 넣었더니 alert 40이 돌아왔다. 어느 존에도 없는 이름도, 와일드카드가 덮지 않는 2단계 서브도메인도 마찬가지였다. Chrome에서는 -113이다.

Cloudflare 문서는 ERR_SSL_VERSION_OR_CIPHER_MISMATCH(Firefox는 SSL_ERROR_NO_CYPHER_OVERLAP)의 원인으로 네 가지를 든다.

원인 문서의 조치
인증서 활성화 지연 (도메인 활성화 후 15분~24시간) Edge Certificates에서 상태가 Active가 될 때까지 기다림. 급하면 Cloudflare를 일시 중지
DNS 레코드가 프록시되지 않음 (DNS only) Proxied로 변경
업로드한 커스텀 인증서가 만료 새 인증서 업로드
다단계 서브도메인 (예: test.dev.example.com) Total TLS 활성화, Advanced certificate 주문, 커스텀 인증서 업로드

마지막 줄은 설정만 보고도 판단할 수 있다. Universal SSL은 full setup에서 apex와 1단계 서브도메인(example.com, www.example.com)만 덮는다. dev.www.example.com 같은 깊은 이름은 “not covered and will not serve a valid certificate”다. CNAME setup 존은 서브도메인마다 Universal SSL 인증서를 따로 받는다.

같은 존의 1단계 이름과 비교해 본다.

openssl s_client -connect www.example.com:443 -servername www.example.com -brief </dev/null
openssl s_client -connect dev.www.example.com:443 -servername dev.www.example.com -brief </dev/null

1단계 이름은 붙고 2단계 이름만 alert 40이면 범위 문제다. 존을 막 추가했는데 모든 이름이 alert 40이면 활성화 지연을 의심하고 Edge Certificates에서 상태를 본다.

원인 5. 오래된 OS와 라이브러리의 기본값

설정 파일에는 문제가 없는데 플랫폼이 TLS 1.2를 기본으로 끄고 있거나 아예 모르는 경우다.

Windows Server와 IIS

Windows의 TLS 구현은 Schannel이다. Microsoft의 TLS 레지스트리 문서는 OCSP 스테이플링 항목에서 IIS와 http.sys 기반 서비스를 이 Schannel 설정의 적용 대상으로 적는다. 버전별 기본값은 이렇다.

Windows TLS 1.1·1.2 (서버) TLS 1.3
Windows Server 2008 미지원 미지원
Windows Server 2008 SP2 지원, 기본 Disabled 미지원
Windows 7 / Server 2008 R2 지원, 기본 Disabled 미지원
Windows 8 / Server 2012, 2012 R2 Enabled 미지원
Windows Server 2016, 2019 Enabled 미지원
Windows Server 2022, 2025 / Windows 11 Enabled Enabled

Windows Server 2008 R2의 IIS를 손대지 않고 쓰면 TLS 1.0 서버가 되고, 최신 브라우저에서 -113이 난다. 거꾸로 Microsoft는 2024년 Windows 11 Insider Preview부터 TLS 1.0과 1.1을 기본으로 끈다고 밝혔다(출시된 OS 버전에는 영향 없음).

시스템 기본값을 Enabled로 덮어쓰려면 버전별 하위 키에 DWORD Enabled를 1로 만든다(Microsoft 문서).

HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server
    Enabled (DWORD) = 1

같은 문서는 레지스트리를 직접 고치기보다 그룹 정책 같은 도구를 먼저 쓰라고 권하고, 문서에 없는 Schannel 키는 만들지 말라고 경고한다. 바뀐 설정은 그 뒤에 새로 연 자격 증명 핸들부터 적용된다. 서비스가 핸들을 재사용하므로 서비스나 앱을 재시작해야 할 수 있다.

암호 스위트 순서는 그룹 정책, MDM, PowerShell로 정한다. PowerShell TLS 모듈에 Get-TlsCipherSuite, Enable-TlsCipherSuite, Disable-TlsCipherSuite가 있다.

Get-TlsCipherSuite | Format-Table Name

TLS 1.3은 Windows Server 2022부터다. Microsoft는 그 이전 Windows에서 TLS 1.3을 켜는 것을 안전한 구성이 아니라고 적는다. 2012~2019는 TLS 1.2로 맞춘다. IIS 인증서 교체 절차는 IIS 가이드에 있다.

오래된 OpenSSL

nginx와 Apache 문서 모두 TLS 1.1·1.2는 OpenSSL 1.0.1 이상, TLS 1.3은 1.1.1 이상이 필요하다고 적는다. 그보다 오래된 OpenSSL과 묶인 웹서버는 ssl_protocols에 무엇을 적어도 TLS 1.0을 넘지 못한다. 설정으로 풀리는 문제가 아니다. nginx -V와 openssl version으로 확인하고 OS나 빌드를 올린다.

Java 클라이언트

Java는 클라이언트 쪽에서 같은 벽을 만난다. 8u291과 11.0.11에서 TLS 1.0과 1.1이 기본 비활성이 됐다. 릴리스 노트는 jdk.tls.disabledAlgorithms에서 TLSv1과 TLSv1.1을 지우면 다시 켤 수 있다고 하면서 “at your own risk”를 붙였다. 상대 서버가 바뀌어야 할 문제를 내 JVM 전체의 보안을 낮춰 덮는 셈이다. JVM의 인증서 신뢰 문제는 Java에서 PKIX path building failed가 날 때에 따로 적었다.

원인 6. 443에서 TLS가 아닌 것이 답한다

Chrome이 따로 분류하지 않은 실패는 모두 -107, 즉 ERR_SSL_PROTOCOL_ERROR가 된다. 서버 쪽에서 먼저 볼 것은 443에서 TLS가 아닌 것이 응답하는 경우다.

443에서 평문 HTTP

nginx에서 listen 443;에 ssl이 빠진 경우가 대표적이다. 2026-10-04에 nginx 1.31.6으로 재현했다.

$ openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
...error:0A00010B:SSL routines:tls_validate_record_header:wrong version number:...

같은 포트에 http://로 붙으면 페이지가 그대로 나온다. 확인은 이게 가장 빠르다.

curl -sS -o /dev/null -w '%{http_code}\n' http://example.com:443/

HTTP 상태 코드가 찍히면 443에서 평문 HTTP가 돌고 있다. 서버에도 흔적이 남는다. nginx 접근 로그에 ClientHello 바이트가 요청처럼 찍히고 400으로 끝난다.

"\x16\x03\x01\x06\x04\x01\x00\x06\x00\x03\x03..." 400 157 "-" "-"
grep -F '\x16\x03' /var/log/nginx/access.log | tail -3

클라이언트마다 보이는 모습은 이렇다.

  • Firefox: SSL_ERROR_RX_RECORD_TOO_LONG. HTTP/1.1 응답의 앞 5바이트를 레코드 헤더로 읽으면 길이 자리가 P/(0x502F, 20527)라서 NSS의 레코드 길이 상한 검사에 걸린다
  • Java: Unsupported or unrecognized SSL message
  • macOS 기본 curl(LibreSSL): tlsv1 alert protocol version. 버전 불일치 때와 같은 문구라 속기 쉽다. 판별은 OpenSSL의 s_client로 한다

listen 443 ssl;로 고친다(IPv6도 쓰면 listen [::]:443 ssl;). 로드밸런서가 443을 TCP로 그대로 넘기는 구성이면 뒤쪽 백엔드 포트가 TLS로 열려 있는지 본다.

443에 다른 서비스

SSH를 443으로 열어 두었거나 포트 포워딩이 엉뚱한 서비스로 가는 경우다. SSH 배너를 보내는 서버에 s_client로 붙어도 평문 HTTP 때와 똑같이 wrong version number가 나왔다. 서버에서 실제로 443을 잡고 있는 프로세스를 본다.

sudo ss -ltnp | grep ':443 '

중간 장비와 프록시

서버는 정상인데 특정 망에서만 깨지면 중간 장비를 의심한다. Chromium 소스에 근거가 둘 있다.

access_denied 경고는 원래 클라이언트 인증서 접근 제어용인데, 페이지를 차단하는 일부 방화벽이 이 경고를 쓴다. Chrome은 CertificateRequest를 받지 않은 상태에서 이 경고가 오면 -107로 바꾼다(crbug.com/630883).

ERR_TLS13_DOWNGRADE_DETECTED(-180)는 TLS 1.3 다운그레이드 방어에 걸린 것이다. 소스 주석은 TLS 1.2를 잘못 구현한 TLS 종료 프록시 뒤에 있을 때도 이 에러가 날 수 있다고 적는다.

같은 주소를 다른 망(휴대폰 테더링 등)에서 열어 보고, 실패하는 망 안에서 s_client를 돌려 결과를 비교한다. TLS를 복호화하는 보안 장비라면 인증서 발급자가 바뀌어 보이기도 한다. 그때는 ERR_CERT_AUTHORITY_INVALID 원인별 해결의 TLS 검사 장비 항목을 본다.

클라이언트 인증서를 요구하는 서버

클라이언트 인증서(mTLS)를 요구하는 서버도 핸드셰이크에서 끊는다. 다만 Chrome은 이 경우를 보통 -107이 아니라 ERR_BAD_SSL_CLIENT_AUTH_CERT(-117)로 보여 준다. TLS 1.3의 certificate_required(116) 경고는 Chrome에서 곧바로 -117이 된다. TLS 1.2 서버는 대개 일반 handshake_failure를 보내는데, Chrome은 CertificateRequest를 받았고 인증서를 보내지 않은 상태에서 이 경고가 오면 -117로 바꾼다(crbug.com/646567).

openssl s_server -Verify 1로 재현하니 TLS 1.2는 alert 40으로 끝났다. TLS 1.3에서는 s_client가 CONNECTION ESTABLISHED까지 찍은 뒤에 경고를 받았다.

CONNECTION ESTABLISHED
...error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:...:SSL alert number 116

TLS 1.3에서는 클라이언트가 보기에 핸드셰이크가 끝난 뒤에 거절이 온다. “연결은 되는데 바로 끊긴다”로 보이면 이 경우를 의심한다. 재현할 때 서버가 인증서를 요청하자 -brief 출력에 Requested Signature Algorithms 줄이 함께 찍혔다. 일반 서버에서는 이 줄이 없었다.

Chrome이 에러 코드를 고르는 방식

Chromium의 net_error_list.h에 있는 정의다.

에러 번호 소스 주석
ERR_SSL_PROTOCOL_ERROR -107 An SSL protocol error occurred.
ERR_SSL_VERSION_OR_CIPHER_MISMATCH -113 The client and server don’t support a common SSL protocol version or cipher suite.
ERR_BAD_SSL_CLIENT_AUTH_CERT -117 The SSL handshake failed because of a bad or missing client certificate.
ERR_SSL_UNRECOGNIZED_NAME_ALERT -159 The SSL server sent us a fatal unrecognized_name alert.

TLS 1.0/1.1 서버에 한때 쓰던 ERR_SSL_OBSOLETE_VERSION(-218)은 지금 쓰지 않는다. 같은 파일의 주석이 “TLS 1.0/1.1 instead cause SSL_VERSION_OR_CIPHER_MISMATCH now”라고 적는다. 오래된 글에서 -218을 보면 지금 Chrome에서는 -113으로 나온다고 읽으면 된다.

BoringSSL 에러를 넷 에러로 바꾸는 함수(openssl_ssl_util.cc의 MapOpenSSLErrorSSL)를 보면 -113으로 가는 경우는 좁다.

  • 서버가 protocol_version(70) 또는 insufficient_security(71) 경고를 보냄
  • 서버가 고른 버전을 클라이언트가 받지 않음 (UNSUPPORTED_PROTOCOL)
  • 공통 스위트가 없음 (NO_SHARED_CIPHER, NO_CIPHER_MATCH)
  • ClientHello에 대한 응답으로 handshake_failure(40) 경고가 옴

갈림길은 마지막 줄이다. 같은 alert 40이라도 ClientHello 직후에 오면 -113, 핸드셰이크 뒤쪽에서 오면 -107이다. BoringSSL은 ServerHello를 기다리던 중에 이 경고를 받으면 HANDSHAKE_FAILURE_ON_CLIENT_HELLO를 따로 붙이고, Chrome은 그 표시를 보고 -113으로 바꾼다. 이 함수의 대응표에 없는 에러는 전부 기본값 -107로 떨어진다. TLS로 해석할 수 없는 바이트, internal_error(80) 경고, 레코드 형식 오류가 모두 여기에 속한다.

Chrome이 받아 주는 범위도 소스에 고정돼 있다. 기본 최소 버전은 TLS 1.2, 최대는 TLS 1.3이다(ssl_config.cc). 소켓 초기화에서 1.2 미만이 설정되면 “TLS versions before TLS 1.2 are no longer supported” 주석과 함께 거부한다. 암호 스위트 문자열 ALL:!aPSK:!ECDSA+SHA1:!3DES는 3DES를 뺀다. Chromium 기반인 Edge도 같은 에러 코드를 쓴다.

다른 클라이언트에서는

상황 Firefox (NSS) Java (JSSE) curl (macOS 기본, LibreSSL)
버전 불일치 SSL_ERROR_UNSUPPORTED_VERSION 또는 SSL_ERROR_PROTOCOL_VERSION_ALERT The server selected protocol version ... is not accepted by client preferences ... 또는 Received fatal alert: protocol_version tlsv1 alert protocol version (--tlsv1.2를 준 경우, exit 35)
공통 스위트 없음 SSL_ERROR_NO_CYPHER_OVERLAP 또는 SSL_ERROR_HANDSHAKE_FAILURE_ALERT Received fatal alert: handshake_failure sslv3 alert handshake failure (exit 35)
TLS가 아닌 응답 SSL_ERROR_RX_RECORD_TOO_LONG Unsupported or unrecognized SSL message tlsv1 alert protocol version (exit 35)

Firefox는 버전 불일치 두 코드에 “This website might not support the TLS 1.2 protocol, which is the minimum version supported by Firefox.” 안내를 붙인다(현재 소스, 영문 UI).

Safari는 보안 기준에 못 미치는 서버를 “can’t establish a secure connection to the server”라며 열지 않는다고 Apple이 안내했다.

Java는 앱이 TLS 1.0/1.1만 켜 둬서 쓸 프로토콜이 하나도 없으면, 서버에 묻기도 전에 No appropriate protocol (protocol is disabled or cipher suites are inappropriate)로 끝난다.

certdesk 진단에서는

certdesk 진단의 한국 측정 지점은 TLS 1.0까지 내려가며 핸드셰이크를 시도한다(보안 수준 0). 결과는 이렇게 나뉜다.

  • TLS 1.0/1.1로만 붙으면 판정은 ‘위험’, 사유는 “TLS 1.2 미만만 지원해 최신 브라우저가 접속하지 못함”이다
  • 어떤 버전으로도 핸드셰이크가 안 되면 판정은 ‘측정 불가’, 사유는 “TLS 핸드셰이크 실패”이고 상세 보기에 원문이 나온다. 2026-10-04에 rc4.badssl.com을 넣었을 때 상세는 [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure (_ssl.c:1010)였고, 3des.badssl.com도 같은 판정이었다. 서버 인증서를 받지 못했으니 만료일은 CT 로그 기준 ’추정’으로만 붙는다
  • 웹서버 보안 점검에는 “TLS 1.0/1.1 비활성” 또는 “레거시 프로토콜 허용: TLS 1.0, TLS 1.1”, 그리고 “TLS 1.3 지원” 또는 “TLS 1.3 미지원 (1.2 까지만)“이 나온다

구형 스위트 전용 서버를 따로 구분해 알려 주지는 않는다. ’측정 불가: TLS 핸드셰이크 실패’가 나오면 앞의 s_client, nmap 순서로 이어 간다. 검사는 443 포트만 한다. 측정 지점에서는 정상인데 특정 사무실에서만 -107이 나면 원인 6의 중간 장비 쪽이다.

참조