문제
요청을 실행하면 curl이 두 단어와 숫자 하나로 멈춥니다:
curl: (35) SSL connect error
또는 curl: (35) OpenSSL SSL_connect: ... in connection to host:443 같은 변형. 페이지도, 인증서 경고도 없이 그냥 핸드셰이크가 죽습니다. 함정은 이겁니다: 인증서 문제처럼 보여서 사람들이 -k를 꺼내는데, 안 통하고, 그대로 막힙니다. 35번은 신뢰의 문제가 아닙니다. 애초에 두 기계가 TLS를 어떻게 말할지 합의하지 못한 것 — 이걸 알고 나면 탐색 범위가 확 줄어듭니다.
35 오류가 실제로 뜻하는 것
curl 내부 이름은 CURLE_SSL_CONNECT_ERROR: TLS 핸드셰이크 어딘가에서 문제가 발생했다는 것입니다. 핵심 단어는 핸드셰이크입니다. curl이 요청 한 바이트를 보내기 전에 양쪽은 협상을 합니다 — hello 메시지, 합의된 프로토콜 버전, 합의된 암호군, 그다음 인증서 교환. 35번은 그 협상이 끝나기 전에 무너질 때 뜹니다.
이웃과 나란히 놓으면 전부 이해됩니다:
- 60번(
CURLE_PEER_FAILED_VERIFICATION): 핸드셰이크가 성공했습니다. curl이 인증서를 받았는데 신뢰할 수 없다고 판단한 것 — 중간 인증서 누락, 낡은 CA 번들, MITM 프록시. curl (60) 가이드에서 다룹니다. - 35번: 핸드셰이크가 실패했습니다. 양쪽이 버전이나 암호군에 합의하지 못했거나 협상 도중 회선이 죽어, curl이 판단할 인증서를 받지도 못했습니다.
그 차이가 진단의 전부입니다. 60은 당신의 신뢰 저장소에, 35는 프로토콜 계층에 삽니다. -k를 붙이는 반사 반응이 여기선 순전히 미신인 이유입니다 — -k는 검증을 끄는데, 애초에 도달할 검증 단계가 없었으니까요.
무엇이 핸드셰이크를 깨는가
거의 모든 35는 이 중 하나입니다:
1. 공통 프로토콜 버전 없음. 서버는 TLS 1.2나 1.3을 요구하는데 로컬 TLS 라이브러리는 1.0/1.1만 내놓거나(그 거울상: 1.2가 상한인 서버에 --tlsv1.3을 강제). 현대 서버는 수년째 TLS 1.0/1.1을 버려 왔고, 오래된 OS는 1.2+에 못 닿는 옛 OpenSSL을 탑재합니다. 핸드셰이크가 시작 전에 끝납니다.
2. 공통 암호군 없음. 버전이 같아도 양쪽은 최소한 하나의 암호군을 공유해야 합니다. 현대 AEAD 암호군만 내놓는 하드닝된 서버와 레거시만 내놓는 낡은 클라이언트는 공통점이 없어, 서버가 ClientHello에 치명 handshake_failure 알림으로 답합니다.
3. 협상 도중 연결이 끊김. 방화벽·DPI 미들박스·잘못 설정된 프록시가 협상 중 TCP 연결을 리셋합니다. curl은 핸드셰이크 중단을 보고 35를 보고합니다 — 양쪽 엔드포인트의 TLS엔 아무 문제가 없어도 말이죠. 회사 네트워크와 공격적인 “보안” 장비가 이걸 상습적으로 합니다.
4. 평문 포트에 TLS를 말함(또는 그 반대). https://host:80을 치거나 평문 HTTP를 서빙하는 포트에 붙으면, curl이 TLS를 시작하려는데 TLS hello가 아닌 바이트를 받습니다. 첫 패킷부터 협상이 말이 안 됩니다.
5. 낡거나 깨진 로컬 TLS 스택. 오래됐거나 어긋났거나 반쯤 업그레이드된 OpenSSL/LibreSSL은, 최신 스택이라면 멀쩡히 끝낼 핸드셰이크를 실패시킵니다 — “다른 데선 다 되는데 이 옛날 서버만” 고전.
읽는 법: curl -v가 승부처다
-v로 다시 실행해 TLS 줄을 읽으세요. 어떤 버전을 내놨는지, 핸드셰이크가 정확히 어디서 죽었는지 알려줍니다:
curl -v https://example.com
SSL connection using TLSv1.x 줄 — 또는 그 부재 — 를 찾으세요. 협상된 버전이 아예 안 보이면 양쪽이 버전 합의에 이르지 못한 것입니다. 그다음 가설을 고정(pin)으로 검증합니다:
curl --tlsv1.2 --tls-max 1.2 https://host— 1.2 강제로 되면 버전 범위 불일치가 문제였습니다.- 다른 네트워크(집 vs 사무실, 또는 클라우드 셸)에서 같은 요청을 비교하세요. 다른 데선 되면 첫 네트워크의 미들박스가 핸드셰이크를 리셋한 것 — 서버가 아니라 3번 원인입니다.
- 평문 TLS 프로브로 올바른 포트인지 확인하세요. “포트가 틀린” 35는 ServerHello 대신 쓰레기 바이트로 즉시 드러납니다.
서버 쪽인가 내 쪽인가? 먼저 가른다
가장 유용한 한 수는 잘못이 서버인지 내 기계인지 정하는 것입니다. 정반대 해결로 갈리기 때문입니다:
- 모든 클라이언트·네트워크에서 동일하게 실패 → 서버 TLS 설정이 문제(내 세계가 못 맞추는 버전/암호군을 요구하거나 설정이 틀림). 서버에서 고치거나, 내 쪽 TLS 라이브러리를 서버에 맞게 올리세요.
- 한 기계 또는 한 네트워크에서만 실패 → 로컬 문제: 낡은 TLS 라이브러리, 또는 그 네트워크의 미들박스/프록시가 핸드셰이크를 죽임. 서버를 건드릴 게 아니라 curl의 TLS 스택을 올리거나 그 네트워크를 벗어나는 게 해결입니다.
중립 지점에서 호스트로 TLS 핸드셰이크를 끝내는 외부 점검이면 이걸 한 번에 답합니다: 바깥에선 핸드셰이크가 성공하는데 내 기계에선 35라면, 서버는 멀쩡하고 문제는 나와 서버 사이에 있습니다.
DechoNet으로 확인하기
- SSL / TLS 점검은 우리 네트워크에서 호스트로 핸드셰이크를 실행해 실제로 협상하는 프로토콜 버전과 암호군을 보고합니다. 여기선 깔끔히 연결되는데 당신의 curl이 35를 낸다면, 서버는 멀쩡하고 실패는 로컬입니다 — 버전이 묶인 TLS 라이브러리나 네트워크 미들박스.
- 포트 점검은 포트가 열려 있고 기대한 것을 말하는지 확인해, 암호군을 쫓기 전에 “평문 포트에 TLS”와 “아무것도 리스닝 안 함” 원인을 배제합니다.
해결 체크리스트
-
-v로 다시 실행해 협상된 TLS 버전 줄을 찾거나, 없다는 것을 확인하세요. -
--tlsv1.2/--tls-max 1.2를 테스트로 고정하세요. 연결되면 버전 범위 불일치 — 강제 플래그로 사는 게 아니라 TLS 라이브러리를 올려 고치세요. - 다른 네트워크에서 같은 요청을 실행하세요. 다른 데서 되면 로컬 미들박스/프록시가 핸드셰이크를 리셋하는 것.
- 스킴과 포트가 맞는지 확인 — 평문 포트에
https금지, 그리고 포트 점검으로 포트가 실제 열렸는지 확인. - OpenSSL/LibreSSL이 오래됐다면 curl과 TLS 라이브러리를 업그레이드하세요.
-k는 꺼내지 마세요 — 그건 35가 아니라 60을 다룹니다.
언제 확대할까
- 외부 점검이 호스트로 TLS를 깔끔히 협상하는데도 내 기계가 35를 낸다면, 서버 건드리기를 멈추세요 — 해결은 내 쪽(TLS 라이브러리 또는 네트워크 미들박스)이고, 서버 설정을 바꾸면 지금 잘 되는 클라이언트만 깨집니다.
- 회사 네트워크의 협상 중 리셋이라면 방화벽/DPI 정책이지 curl 플래그로 덮을 수 있는 게 아닙니다. 클라이언트 수정이 아니라 네트워크 운영자와의 대화입니다.
- 버전을 고정해 보니 서버 상한이 TLS 1.0/1.1이라면, 진짜 문제는 은퇴한 프로토콜을 서빙하는 서버입니다 — 같은 협상 실패의 브라우저 이름인 ERR_SSL_VERSION_OR_CIPHER_MISMATCH를 보세요.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.