이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- TLS handshake가 HTTP 요청 전에 어떤 일을 끝내는가?
- handshake 지연과 실패를 백엔드 장애에서 어떻게 분리하는가?
- TLS 1.2, TLS 1.3, ALPN, session resumption을 실무에서 어떻게 바라보는가?
개요
TLS handshake는 클라이언트와 서버가 HTTP 메시지를 주고받기 전에 암호화 방식, 서버 신원, 세션 키를 합의하는 절차다. 백엔드 개발자에게 중요한 점은 handshake가 성공해야 애플리케이션 로그가 남는다는 것이다. 요청이 앱에 안 보이는데 사용자는 “사이트가 안 열린다”고 말한다면 DNS, TCP, TLS 계층에서 이미 실패했을 수 있다.
- 사용자 요청의 첫 latency에는 DNS, TCP connect, TLS handshake가 포함된다.
- TLS 1.3은 일반적으로 TLS 1.2보다 왕복 횟수가 적고, 재접속에서는 session resumption으로 비용을 줄일 수 있다.
- HTTP/2 사용 여부는 TLS handshake 중 ALPN으로 협상되는 경우가 많다.
- handshake 오류는
SSLHandshakeException, browser TLS error, LBssl_handshake_error,curl: (35)같은 형태로 드러난다.
원리
TLS handshake를 단순화하면 다음 순서다.
- 클라이언트가
ClientHello를 보내며 TLS version, cipher suite, SNI, ALPN, random 값을 제시한다. - 서버가 선택한 version/cipher, 인증서, key exchange 정보를 응답한다.
- 클라이언트가 인증서 chain과 hostname을 검증한다.
- 양쪽이 세션 키를 만들고 이후 HTTP 메시지를 암호화해서 보낸다.
TLS 1.2에서는 key exchange와 인증서 검증 과정에서 왕복이 더 많고 cipher 선택의 폭도 넓다. TLS 1.3은 오래된 암호군을 제거하고 handshake를 단순화했다. 그래서 운영 정책에서는 “TLS 1.2 이상 허용”처럼 적는 데서 끝내지 말고, 실제로 어떤 client가 있는지와 보안 정책이 어떤 cipher를 허용하는지 같이 봐야 한다.
요청 흐름에서의 위치
Client
-> DNS resolve
-> TCP connect
-> TLS handshake
-> HTTP request
-> CDN / LB / Reverse Proxy
-> ApplicationTLS handshake가 실패하면 애플리케이션 controller, filter, interceptor, access log까지 도달하지 않는다. 반대로 앱 로그가 남아 있다면 최소한 client-to-termination 지점의 TLS는 통과했다는 뜻이다. 단, LB에서 TLS가 종료되고 LB-to-App 구간이 다시 TLS라면 두 번째 handshake는 앱 앞단이나 앱 서버에서 따로 실패할 수 있다.
ALPN과 HTTP 버전
ALPN은 TLS handshake 안에서 HTTP/2, HTTP/1.1 같은 application protocol을 고르는 확장이다. 서버가 HTTP/2를 지원한다고 생각했는데 실제로는 HTTP/1.1로 내려가면 connection 수, multiplexing, head-of-line blocking, gRPC 동작이 달라질 수 있다.
openssl s_client -connect api.example.com:443 \
-servername api.example.com \
-alpn h2,http/1.1 </dev/null출력에서 ALPN protocol: h2처럼 확인한다. gRPC를 운영한다면 “포트가 열렸다”보다 “ALPN으로 h2가 협상됐다”가 더 중요한 증거다.
Session Resumption과 0-RTT
Session resumption은 이전 TLS 세션 정보를 이용해 재연결 비용을 줄이는 기능이다. 모바일 네트워크처럼 연결이 자주 끊기는 환경에서는 체감 latency에 영향을 준다. 다만 0-RTT early data는 replay 위험이 있어서 결제, 주문, 상태 변경 요청에는 조심해야 한다.
- 조회 API는 0-RTT의 이득을 검토할 수 있다.
- 멱등성이 없는 POST는 replay 가능성을 먼저 따져야 한다.
- CDN이나 LB에서 지원하더라도 backend가 요청 재처리를 견딜 수 있는지 확인해야 한다.
백엔드에서 보이는 증상
- Spring
RestTemplate,WebClient, Apache HttpClient에서SSLHandshakeException이 발생한다. - 브라우저는
ERR_SSL_VERSION_OR_CIPHER_MISMATCH,ERR_CERT_COMMON_NAME_INVALID,ERR_CERT_AUTHORITY_INVALID를 보여 준다. - LB 로그에는 target에 전달되지 않은
handshake_failure,tls_alert,client_hello관련 오류가 남는다. - p99 latency가 늘었는데 앱 처리 시간은 그대로라면 connect와 TLS 시간이 늘었는지 봐야 한다.
Handshake 지표 해석
| 증거 | 의미 | 다음 확인 |
|---|---|---|
time_connect 증가 | TCP 연결 전후 지연 | DNS, routing, firewall, SYN backlog |
time_appconnect 증가 | TLS handshake 지연 | cipher, CPU, session reuse, cert chain |
SSLHandshakeException | client가 TLS 협상/검증 실패 | truststore, SAN, protocol, SNI |
ALPN protocol 누락 | HTTP/2 협상 실패 가능 | LB/CDN HTTP/2 설정, gRPC 경로 |
| 앱 로그 없음 | HTTP 이전 계층 실패 가능 | CDN/LB TLS log, browser error, openssl s_client |
이 표는 장애 때 “HTTPS가 안 된다”를 더 작은 질문으로 나누기 위한 것이다. 앱 로그가 비어 있으면 애플리케이션 코드보다 termination 지점의 TLS log를 먼저 봐야 할 수 있다.
코드와 명령으로 확인하기
curl -v --http2 https://api.example.com/health
openssl s_client -connect api.example.com:443 \
-servername api.example.com \
-tls1_3 \
-alpn h2,http/1.1 </dev/null
curl -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' \
-o /dev/null -s https://api.example.com/healthtime_appconnect가 크면 TLS handshake 구간을 의심한다. time_starttransfer만 크면 TLS 이후 앱, proxy, upstream 처리 시간을 본다.
Java 클라이언트에서는 오류 메시지와 truststore를 같이 확인한다.
java -Djavax.net.debug=ssl,handshake \
-jar app.jar운영에서 이 옵션은 로그가 매우 많아지므로 짧은 재현 환경이나 별도 인스턴스에서만 사용한다.
주니어가 자주 하는 오해
- 443 포트가 열려 있으면 TLS도 정상이라고 생각한다.
- 브라우저가 접속되면 Java client와 모바일 앱도 같은 방식으로 성공할 것이라 생각한다.
- gRPC 장애를 보면서 ALPN이나 HTTP/2 협상 결과를 확인하지 않는다.
- 인증서 오류를 모두 “인증서 만료”로만 좁혀 본다.
시니어의 설계 판단 기준
- TLS termination 위치별로 client-to-edge, edge-to-origin, service-to-service handshake를 분리한다.
- TLS 정책 강화 전 실제 client runtime, 파트너사 환경, rollback 가능성을 확인한다.
- latency 분석에서는 DNS/TCP/TLS/first byte를 분리해 앱 성능 문제와 연결 문제를 나눈다.
- 0-RTT나 session resumption은 latency 이득과 replay 위험을 함께 본다.
실전 팁
- handshake 문제를 서버 성능 문제로 단정하지 말고
curl -w로 DNS, TCP, TLS, first byte 시간을 분리한다. - 구형 Android, 사내 보안 프록시, 오래된 Java 런타임은 최신 브라우저와 다른 TLS 정책을 가질 수 있다.
- TLS 정책을 강화할 때는 실제 client 분포를 먼저 본다. B2B API라면 파트너사 런타임 버전 확인이 배포 체크리스트에 들어가야 한다.
- gRPC, HTTP/2, WebSocket은 “HTTPS 접속 성공”만으로 충분하지 않다. ALPN, proxy upgrade, idle timeout을 함께 검증한다.
- 개인 프로젝트에서도 배포 후 smoke test에
curl -v와openssl s_client를 넣어 두면 인증서 갱신 실패를 빨리 찾을 수 있다.
인프라 협업 포인트
- 인프라 팀에 “TLS가 안 됩니다”라고 보내기보다 hostname, client IP 대역, 발생 시각,
curl -v결과,openssl s_client의 protocol/cipher/ALPN/SNI 결과를 함께 보낸다. - TLS 정책 변경 전에는 최소 TLS version, 허용 cipher, HTTP/2 사용 여부, 인증서 갱신 방식, rollback 방법을 같이 정한다.
- LB나 CDN에서 TLS를 종료한다면 앱 팀은 access log에 forwarded proto와 request id가 들어오는지 확인해야 한다.
위험 신호!
- TLS 정책을 강화했는데 파트너사나 모바일 앱의 최소 지원 버전을 모른다.
- 앱 로그에는 실패가 없는데 사용자 오류를 애플리케이션 bug로만 추적한다.
- HTTP/2나 gRPC 장애를 보면서 ALPN 협상 결과를 확인하지 않는다.
- 인증서 갱신은 자동화되어 있지만 LB/proxy reload나 배포 반영 검증이 없다.
확인 질문
확인 질문
- TLS handshake가 실패하면 왜 애플리케이션 로그가 없을 수 있는가?
- HTTP 요청이 암호화 협상 이후에 전송되므로 handshake가 실패하면 앱까지 요청이 도달하지 않기 때문이다.
curl -w에서time_appconnect는 무엇을 보는 데 유용한가?
- TLS handshake 완료까지 걸린 시간을 분리해 connect 이후 TLS 구간의 지연을 확인하는 데 유용하다.
- gRPC 장애에서 ALPN을 확인해야 하는 이유는 무엇인가?
- gRPC는 일반적으로 HTTP/2가 필요하고, HTTP/2 협상은 TLS handshake의 ALPN 결과에 영향을 받기 때문이다.