HTTPS와 TLS의 실제 동작 원리: 핸드셰이크와 인증서

Security2026-10-27TryQuickToolBox

SSL 인증서를 추가하고 도메인을 서버에 연결하면 자물쇠 아이콘이 나타납니다. 대부분의 개발자는 여기서 멈춥니다. 그러다 어느 날 API 클라이언트가 UNABLE_TO_VERIFY_LEAF_SIGNATURE 오류로 실패하거나, 모바일 앱이 구형 안드로이드에서 인증서를 거부하거나, 로드 밸런서가 TLS를 종료해서 애플리케이션이 모든 것을 일반 HTTP로 인식하는 상황이 발생합니다. 그때 비로소 TCP 연결과 첫 HTTP 요청 사이에 실제로 무슨 일이 일어나는지 알아야 합니다.

이 글에서는 HTTPS와 TLS가 실제로 어떻게 작동하는지 살펴봅니다: 핸드셰이크가 하는 일, 인증서 유효성 검사 방법, TLS 1.3에서 달라진 점, 그리고 실제로 마주치는 오류를 진단하는 방법을 다룹니다.

HTTPS가 실제로 추가하는 것

HTTPS는 TLS 연결을 통해 전송되는 HTTP입니다. TLS는 세 가지 보장을 제공합니다:

목록에 없는 것에 주목하세요: 기본적으로 클라이언트는 인증되지 않습니다. TLS는 서버를 인증하고, 애플리케이션이 사용자를 인증합니다.

TLS 1.2 핸드셰이크 단계별 설명

TLS 1.2는 구형 인프라와 많은 기업 프록시에서 여전히 흔히 사용되므로 이해할 가치가 있습니다. 전체 핸드셰이크는 다음과 같습니다:

  1. ClientHello — 클라이언트가 지원하는 TLS 버전, 임의 값, 암호화 스위트 목록, (해당되는 경우) SNI 호스트 이름을 보냅니다.
  2. ServerHello — 서버가 버전과 암호화 스위트를 선택하고, 자체 임의 값을 반환하며, 인증서 체인을 보냅니다.
  3. 키 교환 — ECDHE를 사용하면 서버가 임시 공개 키를 보내고 클라이언트도 자체 키를 보냅니다. 양쪽은 동일한 공유 비밀을 도출합니다. 서버는 인증서의 개인 키로 핸드셰이크 트랜스크립트에 서명합니다.
  4. Finished — 양쪽이 핸드셰이크 트랜스크립트에 대한 MAC을 보내 변조가 없음을 확인합니다.
  5. 애플리케이션 데이터 — HTTP 요청과 응답이 암호화되어 흐릅니다.

핵심 세부 사항은 임시 키 교환입니다. ECDHE를 사용하면 연결마다 새로운 키 쌍이 생성됩니다. 공격자가 수년간의 트래픽을 기록한 후 서버의 개인 키를 훔치더라도 과거 세션을 복호화할 수 없습니다. 이 속성을 순방향 비밀성(forward secrecy)이라고 하며, 정적 RSA 키 교환이 폐기된 주된 이유입니다.

TLS 1.3이 판도를 바꾼 방법

TLS 1.3은 핸드셰이크를 단일 왕복으로 단순화하고 레거시 알고리즘을 완전히 제거합니다.

측면TLS 1.2TLS 1.3
왕복 횟수 (전체 핸드셰이크)21
키 교환RSA 또는 ECDHEECDHE만 (순방향 비밀성)
암호화 스위트암호, MAC, 키 교환 별도AEAD 스위트만, 예: TLS_AES_128_GCM_SHA256
정적 RSA, CBC, RC4허용됨제거됨
세션 재개세션 ID / 티켓PSK 및 0-RTT 옵션

TLS 1.3에서는 클라이언트가 키 교환 그룹을 추측하여 첫 메시지에 공유 값을 보내므로 서버가 즉시 키 합의를 완료할 수 있습니다. 인증서는 여전히 전송되지만 이제 암호화되므로 수동적 관찰자는 어떤 호스트 이름이 제공되는지 더 이상 볼 수 없습니다. 단, Encrypted Client Hello를 사용하지 않는 한 ClientHello의 SNI만 유출됩니다.

0-RTT 재개를 사용하면 재방문 클라이언트가 첫 비행에 애플리케이션 데이터를 보낼 수 있습니다. 빠르지만 0-RTT 데이터는 재생 공격이 가능하므로 결제와 같은 비멱등 요청에는 절대 사용하지 마세요.

인증서와 체인 유효성 검사

인증서는 공개 키를 호스트 이름에 바인딩합니다. 유효성 검사는 체인을 따라가는 과정입니다:

unable to verify 오류의 가장 흔한 원인은 중간 인증서 누락입니다. 브라우저는 때때로 중간 인증서를 캐싱하여 이 문제를 덮지만, 명령줄 도구와 모바일 앱은 완전히 실패합니다. 항상 리프 인증서를 먼저, 그 다음 중간 인증서를 포함한 전체 체인을 제공하세요. 루트 인증서는 절대 포함하지 마세요.

OpenSSL을 사용한 실용적인 디버깅

문제가 발생하면 클라이언트를 탓하기 전에 서버가 실제로 보내는 내용을 검사하세요.

# 서버가 제시하는 인증서 체인 표시
openssl s_client -connect example.com:443 -servername example.com -showcerts

# TLS 1.3만 협상하고 선택된 암호 목록 표시
openssl s_client -connect example.com:443 -tls1_3

# 한 줄로 인증서 만료 확인
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates -subject -ext subjectAltName

s_client가 verify error:num=21:unable to verify the first certificate를 보고하면 체인이 불완전한 것입니다. 호스트 이름 불일치를 보고하면 SAN 목록을 확인하세요. 와일드카드 인증서는 하나의 레이블만 포함하므로 *.example.com은 api.example.com과 일치하지만 a.b.example.com과는 일치하지 않습니다.

흔한 설정 오류

합리적인 서버 구성

Nginx의 경우 현대적인 기준 구성은 다음과 같습니다:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;

TLS 1.3에서는 서버가 암호를 선택하도록 하세요. 여기서는 클라이언트 선호도가 괜찮은데, 모든 스위트가 강력하기 때문입니다. 티켓 키를 교체하지 않는 한 세션 티켓은 비활성화하세요. 티켓 키가 유출되면 재개가 손상될 수 있습니다.

FAQ

HTTPS는 호스트 이름을 암호화하나요?

기본적으로는 아닙니다. ClientHello의 SNI 확장은 서버가 어떤 인증서를 제시할지 알 수 있도록 평문으로 전송됩니다. Encrypted Client Hello(ECH)가 이를 숨기지만 아직 채택이 제한적입니다. 경로와 쿼리 문자열은 항상 암호화됩니다.

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

거의 항상 중간 인증서 누락입니다. 브라우저는 종종 중간 인증서를 자체적으로 가져오거나 캐싱하는 반면, 더 엄격한 클라이언트는 서버가 전체 체인을 보내도록 요구합니다. openssl s_client -showcerts로 확인하세요.

TLS 1.3이 항상 TLS 1.2보다 빠른가요?

새 연결의 경우 그렇습니다. 두 번 대신 한 번의 왕복이면 됩니다. 재개된 연결에서는 차이가 줄어들고, 0-RTT는 비멱등 요청의 재생 위험을 감수하고 TLS 1.3을 더 빠르게 만들 수 있습니다.

TLS 변경 후 웹 서버를 감사하는 경우 로그 분석을 통해 실제 영향을 확인하는 경우가 많습니다: 깨진 리디렉션, 예상치 못한 4xx 급증, 또는 여전히 HTTP로 접속하는 클라이언트 등입니다. Nginx 로그 분석기에 액세스 로그를 넣어 상태 코드 분포를 확인하고 HTTPS로 업그레이드를 완료하지 않은 클라이언트를 찾아낼 수 있습니다.