EziLog

HTTPS와 TLS의 동작 원리: 연결 과정부터 브라우저 보안까지

홈으로 돌아가기
post11-1-HTTP-vs-HTTPS.png

앞선 글에서 TCP/IP 4계층과 OSI 7 Layer를 통해 네트워크 통신의 기반을 살펴보고, HTTP 1.0부터 3.0까지의 진화를 통해 웹 성능과 연결 방식이 어떻게 변화해왔는지를 정리했다면, 이제는 그 위에서 “어떻게 안전하게 통신할 것인가”라는 문제를 다룰 차례이다.

브라우저와 서버 간의 안전한 데이터 교환은 서비스의 신뢰성을 결정짓는 가장 중요한 요소가 되었고, HTTPS(HyperText Transfer Protocol Secure)는 그 핵심에 위치한다. 이 글에서는 평문 통신의 취약점으로부터 암호화 통신의 기반이 되는 인증서 체인, TLS 핸드셰이크의 패킷 수준 동작 원리, 그리고 프로젝트를 진행하며 마주칠 수 있는 Mixed Content와 HSTS 최적화 이슈까지 HTTPS의 전 과정을 이해해보려 한다.

  • 1장: 왜 HTTPS가 필요한가 (문제 정의 + 해결 방향 힌트)
  • 2장: 그 “신뢰”는 어떻게 보장되는가 (인증서/CA)
  • 3장: 실제로 연결이 어떻게 만들어지는가 (TLS handshake)
  • 4장: HTTPS를 사용하기 위한 브라우저의 노력 

 

1. 왜 HTTPS가 필요한가

웹 브라우저와 서버가 데이터를 주고받는 기본 프로토콜인 HTTP는 본래 데이터를 암호화하지 않은 평문(Plaintext) 상태로 전송하도록 설계되었다. 이러한 설계는 네트워크의 초기 발전 단계에서는 통신의 오버헤드를 줄이고 구조를 단순화하는 데 기여했지만, 인터넷이 대중화되고 복잡한 인프라가 구축된 현대에 이르러서는 치명적인 보안 결함으로 작용하게 되었다.

평문으로 전송되는 데이터는 클라이언트에서 서버로 도달하기까지 거치게 되는 수많은 라우터, 스위치, 프록시 등 중간 노드에 그대로 노출되며, 이는 다양한 네트워크 레이어 공격의 표적이 된다.

post11-2-network_snipping.png

위의 예시 이미지처럼 사용자가 로그인이나 회원가입을 하기 위해 기밀 정보인 password를 포함한 정보를 평문으로 전송하면 공격자는 암호화되지 않은 해당 패킷을 읽어올 수 있다. 이러한 공격을 네트워크 스니핑이라고 하는데 아래에서 조금 더 다뤄보겠다.

 

1.1 보안 위협 : 스니핑과 중간자 공격

평문 HTTP 통신의 가장 대표적인 공격 기법은 스니핑(Sniffing)과 중간자 공격(MITM)이다. 이 두 가지 공격은 데이터의 기밀성과 무결성을 완전히 파괴하여 사용자 경험과 서비스 신뢰도를 심각하게 훼손한다.

스니핑은 공격자가 네트워크 인터페이스를 무차별 모드로 설정하거나 네트워크 패킷 캡처 도구를 활용하여, 브라우저와 서버 사이를 오가는 데이터를 조용히 엿보는 수동적인 형태의 공격이다. 암호화되지 않은 공공 와이파이 환경이나 보안이 취약한 로컬 네트워크망에서 이러한 스니핑 공격이 발생할 경우, 사용자가 입력한 로그인 자격 증명, 세션 쿠키 같은 프라이빗한 데이터가 고스란히 공격자의 손에 넘어간다. 스니핑은 데이터를 조작하지는 않지만, 통신 내용을 탈취하여 읽거나 다음 공격을 준비하는 문제가 될 수 있다.

post11-3-network_snipping2.png

중간자 공격(MITM)은 스니핑에서 한 걸음 더 나아가, 통신 당사자들 사이에 공격자가 비밀리에 개입하여 데이터를 가로채고 능동적으로 조작하는 공격을 의미한다. 클라이언트와 서버는 서로 직접 통신하고 있다고 착각하지만, 실제로는 모든 트래픽이 공격자를 거쳐 지나간다. 공격자는 단순히 데이터를 훔쳐보는 것을 넘어, 브라우저가 수신하는 HTTP 응답 패킷의 본문을 변조할 수 있다.

예를 들어, 정상적인 웹 페이지의 응답 페이로드에 악성 자바스크립트를 몰래 주입하거나, 사용자를 정교하게 위조된 피싱 사이트로 리디렉션하는 행위가 가능하다. 심지어 브라우저가 안전한 HTTPS 연결을 시도하려고 할 때, 이를 중간에서 가로채어 강제로 안전하지 않은 HTTP 연결로 다운그레이드시키는 SSL Stripping 공격이나 세션 하이재킹과 같은 기만적인 공격도 중간자 공격의 일환으로 수행된다.

아래 이미지처럼 사용자는 서버와 계속 통신하고 있다고 믿지만, 사실은 공격자가 요청을 가로채고 악성스크립트가 삽입된 응답을 주는 방식으로 통신을 중계할 수 있다.

post11-4-MITM.png

이러한 네트워크 공격들을 원천적으로 방어하기 위해서는 통신 경로를 암호화하여 데이터의 기밀성을 보장하고, 상대방이 신뢰할 수 있는 대상인지 인증하는 메커니즘이 필수적으로 요구되며, 이것이 바로 HTTPS가 도입된 근본적인 배경이다.

 

1.2 대칭키와 비대칭키

대칭키와 비대칭키 암호화, 이 두 방식은 각기 뚜렷한 장단점을 지니고 있어, 단독으로 사용할 경우 현대 웹이 요구하는 속도와 보안 기준을 동시에 충족할 수 없다. 중간자 공격을 막기 위해 HTTPS는 전송 계층 보안 프로토콜인 TLS를 활용하여 데이터를 암호화한다.

TLS는 대칭키 암호화와 비대칭키(공개키) 암호화라는 두 가지 서로 다른 수학적 접근 방식을 결합한 하이브리드 암호화 모델을 채택하고 있다.

post11-5-symmetric-key-asymmetric-key.png

TLS는 통신의 단계에 따라 두 방식의 장점만을 선택적으로 선택한다.

브라우저가 서버에 처음 접속하는 핸드셰이크 단계에서는, 복잡하지만 키 분배에 안전한 비대칭키 암호화를 사용하여 서버의 신원을 인증하고 향후 통신에 사용할 대칭키(세션키)를 안전하게 교환한다.

비대칭키를 통해 세션키가 브라우저와 서버 양측에 성공적으로 공유되고 나면, 자원 소모가 큰 비대칭키 연산은 역할을 다하고 종료된다.

그 이후 브라우저가 서버로 보내는 무거운 이미지 파일이나 대규모 JSON 데이터 등 실제 HTTP 통신은 속도가 빠른 대칭키 암호화를 통해 처리된다. 이러한 구조는 중간자 공격으로부터 키를 보호하는 강력한 보안성과, 웹 애플리케이션이 요구하는 짧은 지연 시간을 동시에 만족시켰다.

 

1.3 HTTP/2~3가 TLS를 강제하는 이유

이전 포스팅에서는 HTTP 프로토콜의 진화를 지연시간 개선이라는 관점에서 다뤘다. 하지만 성능 개선의 흐름 속에서 암호화 역시 함께 발전해 왔다. HTTP/1.1에서 HTTP/2, HTTP/3로 이어지는 과정에서 웹의 가장 큰 변화 중 하나는 암호화가 더 이상 선택 사항이 아니라 기본 전제가 되었다는 것이다.

과거에는 80번 포트의 HTTP와 443번 포트의 HTTPS를 상황에 따라 선택할 수 있었지만, 오늘날의 웹에서는 사실상 모든 통신이 TLS 위에서 이루어진다. 흥미로운 점은, 이 변화가 단순히 보안을 강화하기 위한 결정만은 아니라는 것이다.

HTTP/2의 공식 명세(RFC)는 평문 방식인 h2c를 여전히 허용한다. 하지만 Chrome 같은 주요 브라우저들은 이를 지원하지 않고, HTTPS 위에서만 HTTP/2를 사용하도록 강제하고 있다. 그 이유는 인터넷 곳곳에 존재하는 미들박스(Middlebox) 때문이다.

네트워크 경로에는 프록시, 방화벽, 캐시 서버처럼 트래픽을 중간에서 처리하는 장비들이 광범위하게 존재한다. 문제는 이들 중 상당수가 “HTTP는 텍스트 기반의 HTTP/1.1일 것”이라는 가정을 전제로 동작한다는 점이다. 이런 환경에서 HTTP/2의 바이너리 프레이밍 같은 새로운 방식이 평문으로 전달되면, 일부 장비는 이를 비정상적인 트래픽으로 오인해 연결을 끊거나 데이터를 변조해버린다.

이처럼 기존 인프라가 새로운 프로토콜을 방해하는 현상을 프로토콜 경직성이라고 한다. 브라우저 벤더들은 이 문제를 우회하기 위해 한 가지 현실적인 선택을 한다. 바로 모든 HTTP/2 트래픽을 TLS로 암호화해버리는 것이다. 암호화된 트래픽은 중간 장비가 내용을 해석할 수 없기 때문에, 결과적으로 “그냥 통과시키는” 경향을 보인다. 즉, TLS는 단순한 보안 계층을 넘어 새로운 프로토콜이 기존 인터넷 위에서 정상적으로 동작하게 만드는 일종의 보호막 역할을 하게 된 것이다.

HTTP/3에서는 이 흐름이 한 단계 더 나아간다. HTTP/3는 TCP 대신 UDP 기반의 QUIC 프로토콜 위에서 동작하는데, 이 QUIC 내부에는 TLS 1.3이 기본적으로 포함되어 있다. 다시 말해, 연결을 맺는 과정 자체가 곧 암호화 과정이며, 이를 분리해서 사용할 수 없다. 결과적으로 HTTP/3에서는 평문 통신이라는 선택지가 구조적으로 존재하지 않는다.

post11-6-HTTP-ver-TLS.png

3줄 정리

  • HTTP/1.1: TLS가 선택 사항이고, 미들박스가 텍스트 기반 트래픽을 그대로 처리
  • HTTP/2: 바이너리 프레이밍이 미들박스에 막히는 문제를 TLS 암호화로 우회 — RFC는 h2c를 허용하지만 브라우저는 거부
  • HTTP/3: QUIC 내에 TLS 1.3이 내장되어, 구조적으로 평문 통신 자체가 불가능

 

2. 그 “신뢰”는 어떻게 보장되는가

TLS 핸드셰이크에서 비대칭키를 사용해 세션키를 안전하게 교환하려면, 한 가지 전제가 반드시 필요하다. 브라우저가 전달받은 공개키가 ‘진짜 서버의 것’임을 신뢰할 수 있어야 한다는 점이다.

만약 이 전제가 깨지면 상황은 단순해진다. 공격자가 중간에서 자신의 공개키를 서버의 것처럼 속여 전달하고, 브라우저가 이를 기반으로 세션키를 암호화한다면 그 순간 통신 내용은 그대로 공격자에게 노출된다. 암호화 자체는 정상적으로 동작하지만, 신뢰 대상이 잘못된 것이다.

이 문제를 해결하기 위해 등장한 것이 디지털 인증서다. 디지털 인증서는 특정 도메인과 공개키의 소유 관계를 제3자가 보증해주는 일종의 전자 신분증이다.

 

2.1 CA와 신뢰의 사슬

그렇다면 브라우저는 이 “전자 신분증”을 어떻게 믿을까? 핵심은 “누가 이 인증서를 보증했는가”에 있다.

디지털 인증서는 단순한 데이터 파일이 아니라, 신뢰할 수 있는 기관(CA, Certificate Authority)이 자신의 개인키로 서명한 결과물이다. 브라우저는 이 서명을 검증함으로써 해당 인증서의 신뢰성을 판단한다.

여기서 중요한 구조가 바로 신뢰의 사슬(Chain of Trust)이다.

  • Root CA: 브라우저와 운영체제에 기본적으로 내장된 “신뢰의 기준점”이다. 이들은 사전에 검증되어 트러스트 스토어에 포함되며, 브라우저는 이들을 무조건 신뢰한다.
  • Intermediate CA: Root CA가 직접 인증서를 발급하는 대신, 신뢰를 위임받아 실제 발급을 담당하는 중간 계층이다. 이는 Root 키를 보호하고, 문제가 발생했을 때 영향을 제한하기 위한 구조다.
  • Server Certificate: 우리가 실제로 접속하는 웹사이트의 인증서로, Intermediate CA에 의해 서명된다.
post11-7-Chain-of-Trust.png

TLS 연결 과정에서 서버는 자신의 인증서뿐 아니라, 이를 서명한 Intermediate 인증서들을 함께 전달한다. 그러면 브라우저는 다음과 같은 방식으로 검증을 수행한다:

  1. 서버 인증서의 서명을 검증
  2. 그 상위 인증서(Intermediate)를 검증
  3. 최종적으로 신뢰 저장소에 있는 Root CA까지 연결되는지 확인

이 검증 경로가 끊기지 않고 이어질 때, 브라우저는 비로소 해당 서버를 “신뢰할 수 있는 대상”으로 판단한다.

 

2.2 브라우저는 어떻게 가짜 서버를 걸러낼까?

브라우저는 인증서 체인을 단순히 “형식적으로” 확인하는 것이 아니라, 해당 인증서가 위조되지 않았음을 암호학적으로 증명하는 과정을 거친다. 이를 서명 검증이라고 한다.

이 과정의 핵심은 복잡해 보이지만 하나로 요약된다. “내가 직접 계산한 값”과 “CA가 서명한 값”이 같은지 비교한다. 이 비교가 성립하는 순간, 인증서는 신뢰할 수 있는 것으로 간주된다.

검증은 인증서 체인을 따라 아래에서 위로 올라가며 반복되며, 기본적인 흐름은 다음과 같다.

  1. 해시 계산 (브라우저가 직접 수행)

    브라우저는 인증서에서 서명 부분을 제외한 본문(TBS Certificate)을 꺼낸 뒤, 명시된 해시 알고리즘으로 직접 해시값을 계산한다. 이 값은 “지금 내가 받은 인증서 내용”을 대표하는 지문이라고 볼 수 있다.

  2. 서명 복호화 (상위 CA의 공개키 사용)

    이제 브라우저는 상위 인증서(Intermediate CA)에 포함된 공개키를 이용해, 현재 인증서에 붙어 있는 디지털 서명을 검증한다. 이 과정은 “CA가 개인키로 서명해둔 값”을 공개키로 확인하는 과정이며, 결과적으로 CA가 생성했던 원본 해시값을 얻어낸다.

  3. 두 해시값 비교 (검증의 핵심)

    브라우저가 직접 계산한 해시값과, 서명을 통해 얻어낸 해시값을 비교한다. 이 두 값이 정확히 일치한다면 두 가지가 동시에 증명된다.

  • 인증서 내용이 중간에 변조되지 않았다 (무결성)
  • 해당 인증서는 실제로 CA의 개인키로 서명되었다 (발급자 신뢰성)
post11-8-browser-signature-verification.png

이 중 하나라도 깨지면 검증은 즉시 실패하고, 브라우저는 연결을 차단한다. 이 과정은 한 번으로 끝나지 않는다. 브라우저는 리프 인증서 → 중간 인증서 → 루트 인증서까지 같은 방식으로 검증을 반복하며, 최종적으로 로컬에 저장된 Root CA까지 신뢰 경로가 이어지는지를 확인한다.

여기에 더해 몇 가지 현실적인 조건들도 함께 검사된다.

  • 인증서의 유효 기간이 지나지 않았는지
  • 접속한 도메인과 인증서의 도메인이 일치하는지
  • 이미 폐기된 인증서가 아닌지

특히 인증서 폐기 여부는 성능과 직결되는 문제다. 과거에는 브라우저가 CA 서버에 직접 질의(OCSP)를 보내 확인했지만, 이 방식은 지연과 프라이버시 문제를 유발했다. 이를 개선하기 위해 현재는 서버가 미리 검증된 OCSP 응답을 받아 TLS 핸드셰이크 시 함께 전달하는 OCSP Stapling 방식이 널리 사용된다.

결국 이 모든 과정은 하나의 질문에 답하기 위한 것이다.

“이 공개키, 정말 우리가 믿어도 되는 대상의 것인가?”

브라우저는 이 질문에 대해 수학적으로 “그렇다”고 답할 수 있을 때만, 이후 TLS 통신을 안전하게 이어간다.

 

3. 실제로 연결이 어떻게 만들어지는가 (TLS handshake)

앞서 브라우저는 인증서 검증을 통해 “이 서버를 믿을 수 있는가”를 판단했다. 이제 남은 문제는 하나이다.

“그럼 이 신뢰할 수 있는 상대와, 어떻게 안전하게 데이터를 주고받을 것인가?”

이 질문에 대한 답이 바로 TLS 핸드셰이크다.

TLS 핸드셰이크는 TCP의 3-Way Handshake로 물리적인 연결이 수립된 직후, 브라우저와 서버가 암호화 통신을 위한 규칙을 협상하고 공유 키를 만들어내는 과정이다. 이 과정에서 비대칭키 암호화는 “안전한 키 교환”을 위해 사용되고, 이후 실제 데이터 전송은 대칭키 암호화로 이루어진다.

과거 TLS 1.2에서는 다양한 키 교환 방식을 지원했지만, 그만큼 협상 과정이 복잡했고 여러 번의 왕복 통신(RTT)이 필요했다. 또한 일부 방식은 현대 기준에서 보안적으로 취약하다는 문제가 드러났다.

이러한 한계를 개선하기 위해 등장한 것이 TLS 1.3이다.

TLS 1.3은 불필요한 선택지를 제거하고, 안전한 알고리즘만을 남기는 방식으로 프로토콜을 단순화했다. 그 결과 핸드셰이크 과정이 크게 줄어들었고, 연결 지연 시간 역시 눈에 띄게 감소했다. 또한 더 많은 구간이 기본적으로 암호화되도록 설계되어 프라이버시 보호 수준도 강화되었다.

이제부터는 현대 웹의 사실상 표준인 TLS 1.3을 기준으로, 브라우저와 서버가 어떤 패킷을 주고받으며 암호화 채널을 만들어가는지 단계별로 살펴본다.

 

3.1 Client Hello부터 Finished까지: 패킷 관점으로 보는 핸드셰이크 흐름

TLS 1.3의 가장 큰 변화는 핸드셰이크 과정을 1번의 왕복 통신(1-RTT)으로 줄였다는 점이다. 그 핵심은 클라이언트가 첫 메시지에서부터 키 교환에 필요한 정보를 미리 보내는 데 있다. 전체 흐름을 패킷 관점에서 보면 다음과 같이 정리된다.

1. Client Hello: 클라이언트의 선제적 제안

post11-9-TLS_handshake-ClientHello.png

브라우저는 연결을 시작하며 Client Hello를 전송한다. 이 메시지에는 다음과 같은 정보가 포함된다.

  • 지원 가능한 암호화 방식 목록
  • 재전송 공격 방지를 위한 난수
  • 그리고 가장 중요한 Key Share (클라이언트의 임시 공개키)

TLS 1.3에서는 이 Key Share가 핵심이다.

클라이언트는 “서버가 아마 이 방식을 쓸 것”이라고 가정하고, 키 교환에 필요한 공개키를 미리 보내버린다. 이 한 번의 선제적 행동이 RTT를 줄이는 결정적인 역할을 한다.

 

2. Server Hello ~ Finished: 서버의 응답과 암호화 시작

post11-10-TLS_handshake-ServerHello.png

서버는 Client Hello를 받는 즉시, 클라이언트가 보낸 Key Share와 자신의 개인키를 이용해 공유 비밀값을 계산한다. 그리고 다음 메시지들을 순차적으로 반환한다.

  • Server Hello
  • 선택된 암호화 알고리즘
  • 서버 난수
  • 서버의 Key Share (서버 공개키)

이 시점부터 클라이언트와 서버는 동일한 키를 계산할 수 있는 상태가 된다.

이후부터는 TLS 1.3의 중요한 특징이 드러난다.

  • Encrypted Extensions & Certificate

    서버는 이제부터의 핸드셰이크 메시지를 대칭키로 암호화하여 전송한다.

    TLS 1.2와 달리, 인증서까지 암호화되어 전달되기 때문에 네트워크 상에서 정보 노출이 크게 줄어든다.

  • Certificate Verify

    서버는 자신의 개인키로 지금까지의 핸드셰이크 내용을 서명하여,

    “이 통신의 주체가 실제 인증서의 소유자”임을 증명한다.

  • Finished

    서버 측 준비가 완료되었음을 알리는 메시지로, 이 역시 암호화되어 전달된다.

 

3. 클라이언트 검증 및 핸드셰이크 종료

post11-11-TLS_handshake-finished.png

브라우저는 서버로부터 받은 암호화된 메시지를 복호화한 뒤,

  • 인증서 체인을 검증하고
  • 서버의 신원을 확인한 다음
  • 동일한 방식으로 공유 비밀값을 계산한다

이로써 클라이언트와 서버는 완전히 동일한 세션키를 갖게 된다. 마지막으로 브라우저가 자신의 Finished 메시지를 전송하면 핸드셰이크는 종료된다. 이 시점 이후부터는 더 이상 비대칭키 연산이 등장하지 않는다. 모든 HTTP 요청과 응답은 공유된 대칭키를 이용해 빠르게 암호화되어 전송된다.

 

3.2 TLS 1.2 vs TLS 1.3: RTT의 차이

post11-12-TLS1.2-vs-TLS1.3.png

프론트엔드 성능 관점에서 TLS 버전 차이는 단순한 보안 문제가 아니라, 사용자가 체감하는 첫 로딩 속도(TTFB)에 직접적인 영향을 준다. 그 핵심 변수는 바로 RTT(Round Trip Time), 즉 패킷이 한 번 왕복하는 데 걸리는 시간이다.

TLS 1.2와 TLS 1.3의 가장 큰 차이는 이 RTT를 몇 번 소비하느냐에 있다. TLS 1.2에서는 핸드셰이크가 두 단계로 나뉜다.

  1. 사용할 암호화 방식 협상
  2. 그 결과를 기반으로 키 교환

이 구조 때문에 최소 2번의 왕복(2-RTT)이 필요하다. 네트워크 환경에 따라 다르지만, 모바일이나 해외 서버의 경우 이 단계에서만 수백 ms가 소모될 수 있다. 반면 TLS 1.3은 이 과정을 하나로 합쳤다. 클라이언트가 Client Hello 단계에서 Key Share를 미리 보내기 때문에, 서버는 별도의 추가 협상 없이 바로 키를 계산할 수 있다. 그 결과 핸드셰이크는 1-RTT로 줄어든다.

단순히 한 번 줄어든 것 같지만, 실제로는 초기 연결 지연이 최대 절반까지 감소한다.

TLS 1.2에서도 세션 재개가 가능하지만, 여전히 1-RTT가 필요하다. 반면 TLS 1.3은 0-RTT를 지원한다. 이는 이전에 연결했던 서버라면, 핸드셰이크를 기다리지 않고 클라이언트가 첫 요청 데이터를 바로 전송할 수 있다는 의미다.

보안성도 크게 개선되었다. TLS 1.2는 지원하는 암호화 방식이 너무 많아 설정이 복잡하고 취약점이 발생할 여지가 있었다. 반면 TLS 1.3은 취약한 구형 방식을 모두 퇴출하고, 검증된 안전한 방식으로만 통일했다. 특히 '전방향 안전성(PFS)'이 기본 적용되어, 나중에 서버의 핵심 암호키가 해킹당하더라도 과거에 주고받은 데이터는 안전하게 보호된다.

또 하나의 중요한 변화는 “무엇이 보이느냐”다. TLS 1.2에서는 인증서를 포함한 핸드셰이크 정보 일부가 평문으로 노출되었지만, TLS 1.3에서는 Server Hello 이후 대부분의 정보가 암호화된다. 이는 단순한 보안 강화가 아니라, 네트워크 상에서 노출되는 메타데이터 자체를 줄이는 프라이버시 개선이기도 하다.

정리하면 TLS 1.3은

  • RTT를 줄여 더 빠르게 연결을 만들고
  • 키 교환 방식을 단순화해 더 안전하게 만들며
  • 핸드셰이크를 암호화해 더 많은 정보를 숨긴다

이 세 가지를 동시에 달성한 프로토콜이다.

post11-13-TLS1.2-vs-TLS1.3-table.png

 

3.3 TLS1.3 세션 재개의 0-RTT

post11-14-TLS1.3-0RTT-session-restart.png

TLS 1.3이 체감 성능을 크게 끌어올리는 이유는, 핸드셰이크 자체를 줄이는 것을 넘어 아예 생략할 수 있는 경로를 제공한다는 점에 있다. 그 대표적인 기술이 0-RTT 기반의 세션 재개다.

기본적으로 클라이언트가 처음 접속하는 서버와는 반드시 1-RTT 핸드셰이크를 거쳐야 한다. 하지만 한 번 연결이 성립되면, 서버는 이후 재접속을 위해 세션 티켓(Session Ticket) 또는 PSK(Pre-Shared Key)를 클라이언트에 전달한다. 이 정보를 활용하면 다음 연결부터는 방식이 달라진다.

브라우저는 Client Hello를 보낼 때, 과거 세션 정보를 기반으로 암호화된 애플리케이션 데이터를 함께 실어 보낼 수 있다. 즉, 핸드셰이크 완료를 기다리지 않고 첫 패킷에서 바로 HTTP 요청을 전송하는 구조다 이렇게 되면 RTT가 1에서 0으로 줄어들고, 재연결 시 TLS 오버헤드는 사실상 사라진다. 특히 모바일 환경이나 API 호출이 잦은 서비스에서는 이 차이가 체감 성능으로 직결된다.

하지만 0-RTT는 구조적으로 한 가지 중요한 한계를 가진다. 재전송 공격에 취약하다 0-RTT 데이터는 서버의 최신 난수를 반영하지 않은 상태에서, 과거 세션 정보를 기반으로 미리 암호화되어 전송된다.

이 때문에 공격자가 이 초기 패킷을 캡처한 뒤, 그대로 다시 서버에 보내는 “재전송”이 가능해진다.서버 입장에서는 이 요청이 정상 클라이언트의 재요청인지, 공격자가 복제한 것인지 구분하기 어렵다. 문제는 이 요청이 “상태를 변경하는 작업”일 때 발생한다.

예를 들면 아래처럼 "은행 계좌에서 10만 원 송금"과 같이 서버의 상태를 변경하는 중요한 API 요청이었다면, 공격자가 패킷을 100번 재전송할 때마다 100번의 결제가 중복으로 일어나는 끔찍한 사태가 발생할 수 있다.

post11-15-TLS1.3-0RTT-vulnerability.png

이 때문에 TLS 1.3 명세와 실제 브라우저/서버 구현은 0-RTT 사용을 엄격하게 제한한다. 클라이언트는 멱등성이 보장되는 요청에만 0-RTT를 사용해야 하며 일반적으로 이는 GET과 같은 읽기 전용 요청에 해당한다.

또한 서버 측에서도 방어가 필요하다. POST, PUT 같은 상태 변경 요청에 대해서는 0-RTT 데이터를 거부하고 일반적인 1-RTT 핸드셰이크로 유도하는 방식이 권장된다. 결국 0-RTT는 이렇게 정리할 수 있다.

  • 속도 관점: RTT를 0으로 만들어 가장 빠른 연결을 제공
  • 보안 관점: 재전송 공격 가능성을 전제로 제한적으로 사용해야 함

즉, “무조건 켜면 좋은 기능”이 아니라 조건을 이해하고 선택적으로 적용해야 하는 최적화 기술이다.

 

4. HTTPS 이슈와 최적화

TLS를 적용했다고 해서, 웹 서비스 전체가 자동으로 안전해지는 것은 아니다. 실제 문제는 브라우저가 렌더링하는 “문서 내부”에서 발생한다. 하나의 HTML 문서 안에서 HTTPS와 HTTP 리소스가 섞이는 순간, 브라우저는 이를 더 이상 안전한 페이지로 간주하지 않는다.

이때 발생하는 대표적인 문제가 바로 Mixed Content다.

 

4.1 Mixed Content 에러: HTTPS 페이지에서 HTTP 리소스를 호출할 때 

Mixed Content는 HTTPS로 로드된 페이지 내부에서 HTTP 리소스를 요청하는 상황을 의미한다. 예를 들면,

  • HTML은 https://로 로드되었지만 <script><img>fetch() 등이 http://로 요청되는 경우

이 상황이 위험한 이유는 명확하다. 페이지 자체는 암호화되어 있지만, 내부에서 불러오는 리소스는 중간자 공격에 그대로 노출되기 때문이다 특히 JavaScript 같은 리소스가 변조될 경우, 해당 스크립트는 부모 페이지와 동일한 권한으로 실행된다.

즉, 공격자는 다음과 같은 행위를 할 수 있다:

  • 세션 쿠키 탈취
  • 사용자 입력 가로채기
  • DOM 조작 및 피싱 UI 삽입
post11-16-mixed_content_error.png

결과적으로 HTTPS가 제공하는 보안 컨텍스트가 내부에서 무너진다. 브라우저는 이러한 위험도를 기준으로 Mixed Content를 두 가지로 나누어 처리한다.

1) Active Mixed Content (능동적 콘텐츠)

  • 대상: <script><link><iframe>fetchXHR 등
  • 특징: 페이지 동작 자체를 변경할 수 있음

이 경우 브라우저는 요청 자체를 아예 차단한다. 네트워크로 나가기 전에 막아버리기 때문에, 개발자 콘솔에서 에러로 바로 확인된다.

2) Passive Mixed Content (수동적 콘텐츠)

  • 대상: <img><video><audio> 등
  • 특징: 직접적인 코드 실행은 없음

최신 브라우저는 이를 단순 경고로 두지 않고, 가능한 경우 HTTP 요청을 HTTPS로 자동 업그레이드한다. 서버가 HTTPS를 지원하면 정상 로드 지원하지 않으면 최종적으로 실패

그렇다면 이 문제는 어떻게 해결하면 좋을까? 사실 가장 확실한 해결책은 단순하다. 모든 리소스를 HTTPS로 통일하는 것이다. 절대 경로를 https://로 수정하거나 프로토콜을 생략한 상대 경로(//example.com/...) 사용하는 것이다.

문제는 레거시 환경이다. DB나 템플릿에 http://가 대량으로 하드코딩되어 있다면, 이를 전부 수정하는 것은 현실적으로 어렵다.이럴 때 사용하는 방법이 CSP의 upgrade-insecure-requests다.

PLAINTEXT
Content-Security-Policy: upgrade-insecure-requests

이 설정을 적용하면 브라우저는 DOM을 파싱하면서 발견하는 모든 모든 HTTP 요청을 자동으로 HTTPS로 변환한 뒤 전송한다. 즉, 코드를 수정하지 않고도 클라이언트 단에서 Mixed Content 문제를 상당 부분 완화할 수 있다.

다만 이 방식에도 전제가 있다. 리소스 서버가 HTTPS를 반드시 지원해야 한다. 그렇지 않으면 업그레이드된 요청은 실패하게 된다. 정리하면 Mixed Content는 HTTPS 보안 모델을 내부에서 무너뜨릴 수 있는 구조적 문제이며 브라우저는 이를 강하게 차단하는 방향으로 진화해왔다.

 

4.2 HSTS : 브라우저가 HTTP 요청 자체를 차단하는 원리

과거 웹에서는 사용자가 example.com을 입력하면 브라우저가 먼저 http://로 요청을 보내고, 서버가 이를 HTTPS로 리디렉션하는 방식이 일반적이었다. 이 구조는 최초 요청이 평문으로 노출된다는 점에서 SSL Stripping 공격에 취약했다.

최근에는 주소창 탐색에서 HTTPS 우선 시도를 점점 강화하고 있다. Chrome등 최신 브라우저들은 HTTPS-First 정책을 기본값으로 채택하여, 사용자가 도메인만 입력하더라도 처음부터 HTTPS로 연결을 시도한다.

즉, 과거처럼 “무조건 HTTP → HTTPS 리디렉션” 흐름은 더 이상 기본 전제는 아니다.

그럼에도 불구하고, 이 방식만으로는 보안이 완전히 해결되지 않는다. HTTPS-First는 어디까지나 “우선 HTTPS로 시도”하는 정책일 뿐, 서버가 HTTPS를 지원하지 않거나 연결이 실패할 경우 브라우저는 여전히 HTTP로 fallback을 시도할 수 있다. fallback이 허용되는 환경에서는, 공격자가 HTTPS 연결 실패를 유도할 경우 HTTP로 downgrade될 가능성이 남는다.

이 문제를 근본적으로 차단하는 메커니즘이 바로 HSTS다. 서버는 HTTPS 응답에 다음과 같은 헤더를 포함시킨다.

PLAINTEXT
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

이 헤더를 한 번이라도 수신한 브라우저는 해당 도메인에 대해 다음과 같이 동작한다:

  • HTTPS 연결이 실패하더라도→ HTTP로 fallback하지 않음
  • 사용자가 http://를 명시적으로 입력해도→ 네트워크로 보내지 않고 내부에서 HTTPS로 강제 변환
  • 인증서가 유효하지 않은 경우→ 예외 없이 연결 차단 (우회 불가)

즉, “HTTPS를 시도한다”가 아니라 “HTTPS만 허용한다”로 정책이 강화된다.

post11-17-HSTS.png

HTTPS-First vs HSTS의 역할 차이

  • HTTPS-First: 기본 연결을 HTTPS로 “우선 시도”, 실패 시 경고 후 사용자 선택으로 HTTP 가능
  • HSTS: HTTP 자체를 금지, fallback 경로까지 완전히 제거

HSTS의 한계와 Preload

여전히 한 가지 문제가 남는다. 브라우저가 HSTS 정책을 알기 위해서는 최소 한 번은 HTTPS 응답을 받아야 한다. 이 TOFU(Trust On First Use) 문제를 해결하기 위해 브라우저는 HSTS Preload List를 사용한다.

이 리스트에 포함된 도메인은 첫 접속부터 HTTPS만 허용, fallback 자체가 불가능하다. 특히 .dev.app 같은 TLD는 이 리스트에 포함되어 있어 처음 접속부터 강제 HTTPS가 적용된다. 그래서 실수로 보안에 문제되는 요청을 차단할 수 있다는 장점이 있다.

정리하자면 이제 브라우저는 기본적으로 HTTPS로 먼저 연결하지만 fallback이 존재하는 한 완전한 방어는 아니다. HSTS는 이 fallback 경로를 제거하는 역할을 하고, Preload까지 적용하면 “첫 요청” 단계까지 완전히 보호된다.

마지막으로 블로그를 쓰면서 읽게된 기사가 있다. 크롬이 최신에 발표한 기사인데 이에 따르면 첫 요청에 보낸 http:// 요청이든, 첫요청에 실패하고 http:// 로 fallback하던 과거의(현재까지의) 동작에서 이젠 "'조용히 HTTP로 넘어가 주는 동작'을 아예 없애버리고, 강력한 경고창으로 일단 연결을 차단하여 사용자 모르게 평문 통신이 이루어지는 것을 원천 봉쇄하겠다는 뜻이다.

 

Comments