LabHub

네트워크 기초 · HTTP 와 TLS · 이론

TLS — 브라우저는 되는데 curl 은 실패하는 이유

LabHub 에서 이어서 보기

한 줄 요약

TLS 검증은 서버가 보낸 인증서 체인을 신뢰 저장소의 루트까지 이어 보는 일이며, 브라우저는 끊긴 사슬을 스스로 메워 주지만 curl 같은 도구는 그러지 않는다.

왜 이게 필요했나

새 인증서를 배포했는데 브라우저에서는 자물쇠가 정상이고 백엔드에서 호출하면 이렇게 실패한다.

curl: (60) SSL certificate problem: unable to get local issuer certificate

이 시점의 가장 흔한 결론은 "curl 이 최신 CA 를 모른다"이고, 그래서 -k 를 붙이거나 검증을 끈다. 이것은 원인을 덮는 조치이며 대개 서버 설정이 잘못된 상태로 운영에 들어가게 만든다.

어떻게 동작하나

핸드셰이크. TLS 1.3 은 왕복 한 번에 끝난다. ClientHello 에 지원 버전, 키 공유, 접속하려는 호스트 이름(SNI), 응용 프로토콜 목록(ALPN)이 실려 가고, ServerHello 이후의 모든 메시지가 암호화된다. 여기서 운영에 직접 영향을 주는 사실 세 가지가 나온다.

첫째, TLS 1.3 에서는 패킷 캡처만으로 서버 인증서를 꺼내 볼 수 없다. 인증서를 확인하려면 클라이언트 쪽 도구를 써야 한다.

둘째, SNI 는 여전히 평문이다. 한 IP 에 여러 사이트가 붙어 있는 요즘 구성에서 서버는 SNI 를 보고 어느 인증서를 내밀지 정한다. openssl s_client 에서 -servername 을 빼면 SNI 가 전송되지 않아 기본 가상 호스트의 엉뚱한 인증서를 보고 잘못된 결론을 내리게 된다.

셋째, 클라이언트가 Finished 와 첫 요청을 연달아 보내므로 상호 TLS 에서 클라이언트 인증서가 거부되면 핸드셰이크 오류가 아니라 첫 요청 직후의 연결 끊김으로 나타난다.

체인 검증. 서버의 책임은 루트를 제외한 중간 인증서를 모두 함께 보내는 것이다. 중간 인증서가 빠지면 클라이언트는 발급자를 신뢰 저장소에서 찾아야 하는데, 저장소에는 루트만 있고 중간 인증서는 없으므로 사슬이 끊긴다.

그런데 브라우저는 왜 성공할까. 종단 인증서 안에는 발급자 인증서를 내려받을 수 있는 URL(AIA 확장)이 들어 있고, 크롬과 사파리는 검증 중 발급자를 못 찾으면 그 URL 로 직접 내려받는다. 반면 OpenSSL 은 기본적으로 이것을 따라가지 않는다. curl, wget, 파이썬 requests, Go, Node 가 대부분 여기에 해당한다.

그래서 다음 명제가 성립한다. 브라우저에서 되는데 curl 에서 실패한다면 클라이언트가 아니라 서버가 잘못된 것이다. 브라우저가 서버의 실수를 대신 메워 주고 있을 뿐이고, 그 대신 첫 접속마다 추가 왕복 비용을 낸다.

확인은 한 명령이면 된다. openssl s_client -connect 호스트:443 -servername 호스트 -showcerts 로 접속해 Certificate chain 에 몇 개가 나오는지 센다. 종단 인증서 하나만 나오면 그 순간 원인은 확정이다.

현장에서 만나는 모습

오류 메시지별로 원인이 거의 정해져 있다.

이어지는 퀴즈에서 확인할 것

브라우저와 curl 의 결과가 갈릴 때 어느 쪽을 고쳐야 하는지, 체인 개수로 무엇을 판별할 수 있는지 확인한다.