HTTPS와 TLS의 실제 동작 원리: 핸드셰이크 완벽 해설

Security2026-10-21TryQuickToolBox

인증서를 추가하고 브라우저에 자물쇠가 표시되면 넘어가기 마련입니다. 그러다 어느 날 API 클라이언트가 SSL_ERROR_RX_RECORD_TOO_LONG 오류로 실패하거나, 모바일 앱이 인증서 체인을 거부하거나, HSTS를 활성화한 후 리디렉션 루프가 나타납니다. 그 순간 HTTPS는 블랙박스처럼 느껴집니다.

이 글은 그 블랙박스를 열어봅니다. TLS 핸드셰이크 중에 실제로 무슨 일이 일어나는지, 인증서가 왜 존재하는지, 대칭 암호화와 비대칭 암호화가 각각 어디에 쓰이는지, 그리고 실제 운영 환경에서 마주칠 수 있는 실패를 어떻게 디버깅할지 단계별로 살펴보겠습니다.

HTTPS의 실제 정체

HTTPS는 별도의 프로토콜이 아닙니다. TLS로 암호화된 연결 위에서 전송되는 일반 HTTP입니다. URL 스킴이 바뀌고 기본 포트가 80에서 443으로 이동할 뿐, 메서드, 헤더, 상태 코드, 쿠키 등 나머지는 모두 동일하게 동작합니다.

TLS는 TCP 위에서 세 가지 보장을 제공합니다:

세 번째 보장을 과소평가하는 경우가 많습니다. 인증 없는 암호화는 무가치합니다. 트래픽을 가로챌 수 있는 공격자는 자신의 키를 제시하기만 하면 되니까요.

TLS 1.3 핸드셰이크 단계별 해설

TLS 1.3은 TLS 1.2에 비해 핸드셰이크를 획기적으로 단순화했습니다. 브라우저와 서버 사이의 흐름은 다음과 같습니다:

  1. ClientHello — 클라이언트가 지원하는 TLS 버전, 암호화 스위트, 랜덤 값, 그리고 Diffie-Hellman 그룹(예: X25519)에 대한 키 셰어를 보냅니다.
  2. ServerHello — 서버가 암호화 스위트를 선택하고 자신의 키 셰어를 보냅니다. 이제 양쪽 모두 동일한 공유 비밀을 도출할 수 있습니다.
  3. 암호화된 확장 및 인증서 — 서버가 인증서 체인을 보내며, 이는 이미 도출된 키로 암호화되어 있습니다.
  4. Finished — 양쪽 모두 핸드셰이크 트랜스크립트에 대한 MAC을 보내 변조가 없음을 증명합니다.
  5. 애플리케이션 데이터 — 설정된 세션 위로 HTTP 요청과 응답이 흐릅니다.

TLS 1.3에서 전체 핸드셰이크는 한 번의 왕복으로 끝나며, 세션 재개 시에는 0-RTT까지 가능합니다. TLS 1.2는 애플리케이션 데이터가 흐르기 전에 두 번의 왕복이 필요했습니다.

키 도출 후 인증서가 전송되는 이유

TLS 1.3에서는 인증서가 암호화됩니다. 즉, 수동적 관찰자는 인증서만으로 클라이언트가 어떤 도메인을 방문하는지 알 수 없어 정보 유출이 줄어듭니다. 또한 첫 패킷에서 평문 인증서를 기대하는 디버깅 도구는 혼란스러워 보일 수 있습니다.

대칭 암호화 vs 비대칭 암호화

흔한 오해는 HTTPS가 모든 것을 서버의 공개 키로 암호화한다는 것입니다. 그렇지 않습니다. 비대칭 암호화는 느리고 크기 제한이 있어, 공유 비밀을 설정하는 데만 사용됩니다.

단계암호화 방식목적
키 교환비대칭 (ECDHE)공유 비밀을 전송하지 않고 합의
인증비대칭 서명서버가 인증서 키를 소유함을 증명
대량 데이터대칭 (AES-GCM 또는 ChaCha20-Poly1305)HTTP 트래픽에 대한 빠른 암호화 및 무결성

Diffie-Hellman 키 교환은 순방향 비밀성(forward secrecy)이라는 중요한 특성을 가집니다. 서버의 개인 키가 나중에 유출되더라도, 세션 키는 장기 키에서 파생되지 않았기 때문에 기록된 과거 세션은 복호화할 수 없습니다.

인증서의 내부 구성

X.509 인증서는 공개 키를 도메인 이름에 바인딩합니다. 인증서에는 다음이 포함됩니다:

브라우저는 고정된 루트 인증 기관 집합을 신뢰합니다. 서버는 중간 인증서를 보내고, 브라우저는 리프 인증서에서 신뢰할 수 있는 루트까지 체인을 구축합니다. 중간 인증서가 누락되면 데스크톱 브라우저는 중간 인증서를 캐시하기 때문에 여전히 작동할 수 있지만, 모바일 클라이언트와 명령줄 도구는 실패합니다. 이것이 운영 환경에서 가장 흔한 TLS 실수입니다.

실용적인 디버깅 단계

HTTPS에 문제가 생기면 외부에서 안쪽으로 작업하세요.

  1. 체인 확인: openssl s_client -connect example.com:443 -servername example.com -showcerts로 서버가 리프 인증서만이 아닌 전체 체인을 보내는지 확인합니다.
  2. 만료 확인 — 만료된 인증서는 우회 방법 없이 완전히 실패합니다.
  3. 호스트 이름 확인 — SAN 목록에 클라이언트가 요청한 정확한 호스트 이름(모든 www 접두사 포함)이 있어야 합니다.
  4. 프로토콜 버전 확인 — 레거시 클라이언트가 TLS 1.0만 지원하고 서버가 TLS 1.2 이상을 요구하면 HTTP 전에 핸드셰이크가 실패합니다.
  5. 리디렉션 확인 — HTTPS에서 HTTP로 갔다가 다시 돌아오는 리디렉션은 루프를 만듭니다. 애플리케이션 코드가 아닌 서버 설정에서 수정하세요.
# 인증서 체인 및 협상된 프로토콜 검사
openssl s_client -connect example.com:443 \
  -servername example.com -showcerts < /dev/null 2>&1 | \
  openssl x509 -noout -subject -issuer -dates

# 특정 TLS 버전 테스트
openssl s_client -connect example.com:443 -tls1_2 < /dev/null

Nginx에서 ssl_certificate 지시문은 리프 인증서 다음에 중간 인증서를 이어 붙인 파일을 가리켜야 합니다. 순서를 잘못 연결하면 놓치기 쉬운 체인 오류가 발생합니다.

HSTS와 잘못 설정했을 때의 대가

HTTP Strict Transport Security는 브라우저에게 지정된 기간 동안 해당 도메인에 HTTPS를 사용하도록 지시합니다. 강력하지만 용서가 없습니다. 브라우저가 긴 max-age를 한 번 본 후에는, 나중에 서버에서 HTTPS를 비활성화하더라도 평문 HTTP를 거부합니다.

짧은 max-age로 시작하고, 모든 것이 작동하는지 확인한 후 늘리세요. 모든 서브도메인이 유효한 HTTPS를 제공한 후에만 includeSubDomains를 추가하세요. 프리로딩은 사실상 영구적이므로 확신이 있을 때만 요청하세요.

흔한 오설정

인증서 갱신을 달력 알림이 아닌 배포 파이프라인으로 취급하세요. 갱신에 사람이 필요하다면, 결국 최악의 시점에 실패할 것입니다.

FAQ

HTTPS가 방문하는 도메인 이름을 암호화하나요?

도메인은 TLS SNI 확장에 나타나며 일반적으로 네트워크에서 보입니다. 경로와 쿼리 문자열은 암호화되지만, 네트워크 관찰자는 일반적으로 어떤 호스트에 연결했는지 볼 수 있습니다.

브라우저에서는 사이트가 작동하는데 curl이나 모바일 앱에서는 실패하는 이유는 무엇인가요?

거의 항상 중간 인증서 누락 때문입니다. 브라우저는 이전 방문에서 중간 인증서를 캐시하는 경우가 많지만, curl과 많은 모바일 HTTP 스택은 서버가 전체 체인을 보내도록 요구합니다.

TLS 1.3이 항상 TLS 1.2보다 좋은가요?

최신 클라이언트에게는 그렇습니다. 왕복 횟수가 적고, 암호화 스위트 집합이 작으며, 기본적으로 순방향 비밀성을 제공합니다. 1.3을 협상할 수 없는 클라이언트를 여전히 지원해야 하는 경우에만 TLS 1.2를 활성화하세요.

TLS를 종료하는 서버의 로그를 검사해야 할 때, Nginx 로그 분석기를 빠르게 사용하면 핸드셰이크 실패, 리디렉션 루프, 비정상적인 클라이언트 동작이 장애로 발전하기 전에 발견하는 데 도움이 됩니다.