error:0A00010B:SSL routines:ssl3_get_record:wrong version number
Node.js, Python, curl, Java 등 무엇을 쓰든 OpenSSL 계열 라이브러리에서 같은 문구가 나온다. 메시지가 "버전" 을 말하고 있어 TLS 버전 협상 문제로 읽기 쉽지만 거의 대부분 그것이 아니다.
TLS 레코드의 첫 바이트에는 레코드 종류와 프로토콜 버전이 들어 있다. 클라이언트가 TLS 핸드셰이크를 시작했는데 상대가 돌려준 바이트가 TLS 레코드 형식이 아니면 라이브러리는 버전 필드를 읽다가 말이 안 되는 값을 보고 이 오류를 낸다.
즉 "TLS 로 말을 걸었는데 상대는 TLS 를 하지 않는다" 는 뜻이다. 실제 원인은 다음 중 하나다.
| 상황 | 예 |
|---|---|
| 평문 포트에 HTTPS 로 접속 | https://host:80, https://host:8080 |
| TLS 를 쓰지 않는 프로토콜 포트에 TLS 로 접속 | SSH(22), 평문 SMTP(25), 평문 LDAP(389) |
| STARTTLS 를 써야 하는데 곧바로 TLS 로 시작 | SMTP 587, LDAP 389, PostgreSQL |
| 앞단 프록시가 평문으로 받는데 클라이언트는 TLS 로 보냄 | 로드밸런서 리스너 설정 착오 |
| HTTP 프록시를 TLS 프록시로 지정 | https_proxy=https://proxy:3128 (대개 http:// 가 맞다) |
# TLS 라면 인증서 정보가 나온다
openssl s_client -connect 192.168.0.10:8080 </dev/null
# 평문이면 배너나 HTTP 응답이 그대로 보인다
nc -v 192.168.0.10 8080
curl -v http://192.168.0.10:8080/
SSH 포트에 붙었다면 다음이 나온다.
SSH-2.0-OpenSSH_9.6
이 줄이 보이는데 클라이언트가 TLS 오류를 낸다면 TLS 를 쓰지 않는 서비스에 TLS 로 접속하고 있다는 것이 확정된다.
접속하는 쪽에서 프로토콜과 포트가 짝이 맞는지 본다.
| 잘못된 조합 | 고칠 곳 |
|---|---|
| Protocol 은 SSH 인데 TLS 옵션이 켜져 있음 | TLS 사용 해제 |
포트 22 인데 스킴이 https:// |
스킴을 맞춘다 |
포트 443 인데 스킴이 http:// |
반대 방향 오류. http://...443 은 보통 응답 없이 멈춘다 |
관리 도구에서 호스트를 등록할 때 "SSH 로 붙는 항목" 과 "HTTPS API 로 붙는 항목" 을 혼동해 한쪽 값을 다른 칸에 넣는 실수가 흔하다.
| 메시지 | 뜻 |
|---|---|
wrong version number |
상대가 TLS 를 하지 않음 |
unsupported protocol · no protocols available |
양쪽 다 TLS 인데 지원 버전이 겹치지 않음 |
sslv3 alert handshake failure · no shared cipher |
암호 스위트가 겹치지 않음 |
certificate verify failed |
연결은 됐고 인증서 검증에서 실패 |
unexpected eof while reading |
핸드셰이크 도중 상대가 연결을 끊음 |
두 번째와 세 번째는 진짜 프로토콜·암호 협상 문제다. RHEL 9 계열에서는 시스템 암호 정책이 옛 알고리즘을 막아 발생하는 경우가 많다.
update-crypto-policies --show
sudo update-crypto-policies --set LEGACY # 임시 확인용
원인을 확인했으면 정책을 되돌리고 상대 쪽 설정을 고친다.
포트는 맞는데 시작 방식이 다른 경우다. 평문으로 연결한 뒤 명령으로 TLS 로 올린다.
openssl s_client -starttls smtp -connect mail.example.com:587
openssl s_client -starttls ldap -connect ldap.example.com:389
openssl s_client -starttls postgres -connect db.example.com:5432