LabHub

블로그

TLS 1.3과 QUIC 완벽 가이드 — Handshake, 0-RTT, ECDHE, X25519, HTTP/3 모든 것 (2025)

들어가며 — 매일 쓰지만 아무도 모르는 바이트들

당신은 지금 이 글을 HTTPS로 읽고 있다. 브라우저와 서버 사이에 TLS 핸드셰이크가 일어났고, 수백 바이트의 암호학적 춤이 오간 후 암호화된 채널이 열렸다. 이 과정은 몇 ms 만에 끝났고, 당신은 눈치채지 못했다.

이 "눈치채지 못함"이 TLS의 성공이다. 동시에 비극이기도 하다. 거의 모든 개발자가 HTTPS를 사용하지만, 극소수만이 내부에서 일어나는 일을 안다. 그래서:

이 글은 이 질문들을 바이트 수준에서 답한다. TLS 1.0부터 1.3까지의 진화, 1-RTT 핸드셰이크의 정확한 메시지 흐름, ECDHE와 X25519 키 교환, HKDF 키 스케줄, AEAD 암호 스위트, 0-RTT의 위험, QUIC의 설계 결정, HTTP/3의 QPACK 압축, Connection Migration까지 전부 다룬다. 1,500줄짜리 딥다이브지만 각 절은 독립적이다.

이 글은 Kubernetes 글Observability 글의 전제가 되는 기반 기술을 파헤친다. 클라우드 네이티브 세계의 모든 트래픽이 TLS로 암호화되고, 점점 QUIC 위에서 돌아간다.


1. TLS의 간략한 역사

1.1 SSL에서 TLS까지

1994년 Netscape가 SSL 1.0을 설계 — 너무 결함이 많아 공개도 안 됨. 1995년 SSL 2.0, 1996년 SSL 3.0 (POODLE로 2014년 사망).

1999년 IETF가 TLS 1.0으로 표준화. "Netscape의 SSL과 구분하기 위해 이름을 바꿈" — 사실상 SSL 3.1이지만 정치적 이유로 TLS.

2020년 이후 TLS 1.0/1.1은 브라우저에서 제거됐다. 이제는 1.2와 1.3만 남았고, 1.3이 빠르게 1.2를 밀어내고 있다. Cloudflare 통계(2026년): 트래픽의 80% 이상이 TLS 1.3.

1.2 TLS가 제공하는 것

네 가지 속성:

  1. Confidentiality: 중간자가 내용을 못 읽음 (대칭 암호화).
  2. Integrity: 전송 중 위변조 감지 (AEAD MAC).
  3. Authentication: 서버(선택적으로 클라이언트)의 신원 확인 (인증서 + 서명).
  4. Forward Secrecy: 서버 개인키가 나중에 유출돼도 과거 세션 복호화 불가 (ECDHE).

이 네 가지가 모두 만족되는 구간을 secure channel이라 한다. TLS 1.3은 이 네 가지를 이전 버전보다 엄격히 보장한다.

1.3 핵심 개념 정리


2. TLS 1.2 — 이전 세대

2.1 1.2의 핸드셰이크 (참고용)

TLS 1.3을 이해하려면 1.2가 왜 느렸는지 알아야 한다.

Client                                Server
  |                                     |
  | ClientHello ---------------------->|
  |                                     |
  | <---------------------- ServerHello |
  |             Certificate             |
  |           ServerKeyExchange         |
  |             ServerHelloDone         |
  |                                     |
  | ClientKeyExchange ---------------->|
  | ChangeCipherSpec                   |
  | Finished                           |
  |                                     |
  | <------------- ChangeCipherSpec    |
  |                 Finished            |
  |                                     |
  | Application data <----------------->|

2 왕복 (2-RTT). 실제 데이터 전송 전에 네트워크를 네 번 건너간다. 100ms RTT 환경에서 200ms가 핸드셰이크에만 쓰이는 것.

2.2 1.2의 문제들

  1. 협상 가능한 알고리즘이 너무 많음: ClientHello가 수십 개 cipher suite를 제안. 서버가 고름. 잘못된 선택이 취약점으로 이어짐 (Logjam, FREAK).
  2. 정적 RSA 키 교환이 허용됨: Forward Secrecy 없음.
  3. MAC-then-Encrypt: POODLE, Lucky13 공격의 원인.
  4. Padding oracle: CBC 모드의 구조적 약점.
  5. 핸드셰이크 메시지 평문: 중간자가 SNI(어느 사이트인지), cipher suites를 볼 수 있음.

1.3은 이 모든 문제를 근본부터 뜯어고친다.


3. TLS 1.3 — 1-RTT 핸드셰이크

3.1 메시지 흐름

Client                                Server
  |                                     |
  | ClientHello --------------------->  |
  |   + key_share                       |
  |                                     |
  | <--------------------- ServerHello  |
  |                       + key_share   |
  |                 {EncryptedExtensions}
  |                     {Certificate}   |
  |              {CertificateVerify}    |
  |                      {Finished}     |
  |                                     |
  | {Finished} ----------------------->|
  |                                     |
  | [Application Data] <--------------->|

1 왕복 (1-RTT). 브라켓 {}은 암호화된 메시지를, 대괄호 []는 완전히 암호화된 데이터를 의미한다.

주목할 점:

  1. ClientHello가 key_share를 이미 포함. 서버의 응답을 기다리지 않고 바로 키 교환 재료를 보냄.
  2. ServerHello 후 모든 핸드셰이크 메시지가 암호화됨. 인증서도 암호화. 중간자가 못 봄.
  3. 클라이언트는 서버의 Finished를 받은 후 바로 application data 전송 가능.

RTT 100ms면 — 1.2는 200ms, 1.3은 100ms. 절반으로 단축.

3.2 ClientHello 구조

실제 바이트 레벨 구조:

struct {
    ProtocolVersion legacy_version = 0x0303;   // TLS 1.2
    Random random;                              // 32 bytes
    opaque legacy_session_id<0..32>;
    CipherSuite cipher_suites<2..2^16-2>;
    opaque legacy_compression_methods<1..2^8-1> = { 0 };
    Extension extensions<8..2^16-1>;
} ClientHello;

legacy_version = 0x0303 (TLS 1.2로 표기)! 왜? 중간 장비(proxies, IDS)가 TLS 1.3을 이해 못 해 드롭하는 일이 있었다. 그래서 버전 표기는 1.2로 두고, 실제 1.3은 supported_versions extension으로 알림. 하위 호환을 위한 우회.

핵심 extensions:

3.3 ServerHello와 그 뒤

서버가 받으면:

  1. Cipher suite 선택: ClientHello의 목록에서 하나 고름.
  2. Key share 선택: client_public_key와 자신의 private_key로 shared secret 계산.
  3. ServerHello 전송 (평문): 선택된 cipher suite, 자신의 key_share 포함.
  4. 이 시점 이후 handshake 트래픽 키로 암호화된 메시지 전송:
    • EncryptedExtensions: 나머지 extensions.
    • Certificate: 서버 인증서 체인.
    • CertificateVerify: 인증서 개인키로 지금까지의 핸드셰이크 transcript에 서명.
    • Finished: handshake 완료 확인.

클라이언트는 받아서:

  1. 인증서 체인 검증 (CA 신뢰, 호스트명 일치, 만료 체크, revocation).
  2. CertificateVerify의 서명을 인증서 공개키로 검증 — 서버가 인증서 개인키를 실제로 소유하는지 증명.
  3. 자신의 Finished 전송.
  4. 이제 application data 키로 전환, 실제 데이터 송수신 가능.

3.4 왜 1-RTT가 가능한가

핵심 트릭: 클라이언트가 추측한다.

TLS 1.2: "서버가 어떤 cipher suite를 고를지 몰라 기다림." TLS 1.3: "어차피 ECDHE만 있으니, key_share를 미리 보냄. 서버는 어느 curve를 쓸지만 선택."

협상 가능한 알고리즘이 적어진 것이 속도의 비결. 보안도 더 좋아짐 — 선택지가 적으면 잘못 고를 여지도 적음.


4. Key Exchange — ECDHE와 X25519

4.1 Diffie-Hellman 복습

1976년 Diffie와 Hellman이 발명한 기적의 프로토콜. 공개 채널로 공유 비밀을 합의하는 방법.

전통 DH (modular arithmetic 버전):

1. 공개 파라미터: 큰 소수 p, 생성원 g.
2. Alice: 랜덤 a 선택, A = g^a mod p 전송.
3. Bob: 랜덤 b 선택, B = g^b mod p 전송.
4. Alice는 shared = B^a mod p를 계산. Bob은 shared = A^b mod p를 계산.
5. 수학적 이유로 B^a = g^(ba) = g^(ab) = A^b. 같은.

중간자는 A, B, p, g를 다 보지만 a나 b를 모르므로 shared를 계산 못 함 (discrete log 문제).

4.2 ECDH — 타원 곡선 위의 DH

modular arithmetic 대신 타원 곡선(elliptic curve)의 군(group)에서 같은 계산. 동일 보안 강도에 키 크기가 훨씬 작음.

4.3 ECDHE — Ephemeral

E는 ephemeral. 매 세션마다 새 키 쌍 생성. 이것이 Forward Secrecy의 핵심이다. 세션이 끝나면 private key를 버림. 서버 저장용 장기 키와 무관. 나중에 장기 키가 유출돼도 과거 세션을 복호화 못 함.

TLS 1.3은 ECDHE만 허용. 정적 RSA 키 교환(forward secrecy 없음)은 퇴출.

4.4 X25519 — 현대의 표준

2005년 Daniel J. Bernstein이 설계한 Curve25519 기반. 2019년 TLS 1.3 표준에 명시.

장점:

구현은 매우 간결. OpenSSL, BoringSSL, Go crypto/ecdh 모두 X25519를 기본 권장.

ClientHello의 key_share에 X25519가 있으면 거의 항상 선택됨. 선택지: X25519, secp256r1 (NIST P-256), secp384r1.

4.5 Post-Quantum — 미래

양자 컴퓨터가 ECDHE를 깬다. 대비로 post-quantum 키 교환 연구 진행.

2026년 현재는 하이브리드 모드가 실전 배포. 고전 보안과 양자 저항을 동시에.


5. HKDF — 키 스케줄

5.1 왜 여러 키가 필요한가

"shared secret"은 하나지만, TLS는 여러 키가 필요하다:

각각을 shared secret에서 파생해야 한다. 이 역할을 HKDF (HMAC-based Key Derivation Function)가 한다.

5.2 HKDF의 구조

두 단계:

  1. Extract: 불규칙한 입력(shared secret)을 "깨끗한" 의사난수로. PRK = HMAC(salt, IKM).
  2. Expand: PRK에서 원하는 길이의 여러 키를 파생. OKM = HMAC(PRK, info || 0x01) 등을 반복.

5.3 TLS 1.3의 키 스케줄

                  0 (salt)
                   |
                   v
ECDHE shared -> HKDF-Extract = Handshake Secret
                   |
                   +----> Derive-Secret(., "c hs traffic", ...) = client_handshake_traffic_secret
                   +----> Derive-Secret(., "s hs traffic", ...) = server_handshake_traffic_secret
                   |
                   v
0 (salt) -> HKDF-Extract = Master Secret
                   |
                   +----> Derive-Secret(., "c ap traffic", ...) = client_application_traffic_secret_0
                   +----> Derive-Secret(., "s ap traffic", ...) = server_application_traffic_secret_0
                   +----> Derive-Secret(., "exp master", ...) = exporter_master_secret
                   +----> Derive-Secret(., "res master", ...) = resumption_master_secret

각 "secret"에서 다시 keyiv가 파생된다:

key = HKDF-Expand-Label(secret, "key", "", key_length)
iv  = HKDF-Expand-Label(secret, "iv",  "", iv_length)

레이블이 각 키의 역할을 명시한다. "s hs traffic"은 "server handshake traffic". 한 secret에서 파생된 키는 다른 역할과 분리된다 — cryptographic domain separation.

5.4 Key Update

장시간 연결에서 단일 키로 너무 많은 데이터를 암호화하면 위험. TLS 1.3은 KeyUpdate 메시지로 중간에 키 교체 가능.

current_secret = HKDF-Expand-Label(current_secret, "traffic upd", "", hash_length)

이전 secret에서 새 secret을 파생. 과거 트래픽 복호화는 여전히 불가 (forward secrecy 중첩).


6. AEAD — 암호화와 무결성의 통합

6.1 왜 AEAD만 허용하는가

TLS 1.2의 CBC + HMAC 조합은 여러 공격의 원인이었다:

문제의 근본: 암호화와 무결성이 따로 수행됨 → 상호작용의 복잡성 → 버그.

AEAD는 두 가지를 원자적으로 결합. 암호학적으로 증명된 안전성.

6.2 TLS 1.3의 AEAD 옵션

1.3이 허용하는 cipher suite는 단 5개:

TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256

실전에서는 세 개가 주류:

최신 서버는 세 개 모두 지원. 클라이언트의 선호도에 따라 서버가 결정.

6.3 GCM의 구조

AES-GCM = AES-CTR + GHASH.

  1. CTR (counter) 모드: 카운터 값을 AES로 암호화 → 스트림 생성 → 평문과 XOR. 블록 크기(16B) 의존 없이 임의 길이 평문 처리.
  2. GHASH: 암호문과 associated data에 대해 인증 태그 계산. 개념적으로 Galois Field 위의 다항식 연산.

이 둘이 하드웨어 지원과 잘 맞아 엄청난 속도. Intel/AMD의 AES-NI + PCLMULQDQ 명령어를 쓰면 수 GB/s.

6.4 AEAD의 핵심 규칙: Nonce 재사용 금지

같은 (key, nonce) 쌍으로 두 개의 다른 평문을 암호화하면 치명적. GCM에서는 전체 키 복구까지 가능.

TLS 1.3의 Nonce 구성:

nonce = record_sequence_number XOR iv

record_sequence_number는 매 record마다 +1. iv는 HKDF로 파생된 고정값. 따라서 한 세션 내에서 nonce는 절대 반복되지 않음. 단, sequence가 0으로 wrap around되면 위험 → 그 전에 새 KeyUpdate.


7. 인증서와 Web PKI

7.1 X.509 인증서

인증서(cert)는 "이 공개키는 이 도메인 소유다"라는 공증 문서.

Certificate
├── Subject (누구의 것)
│   └── CN=example.com, O=Example Inc, ...
├── Issuer (누가 서명했는가)
│   └── CN=DigiCert SHA2 Secure Server CA
├── Validity (유효 기간)
│   ├── NotBefore: 2026-01-01
│   └── NotAfter: 2026-04-01
├── SubjectPublicKeyInfo
│   └── algorithm: ECDSA P-256, public_key: 0x04a7...
├── Extensions
│   ├── SAN (Subject Alternative Names): *.example.com, example.com
│   ├── KeyUsage: Digital Signature, Key Encipherment
│   ├── ExtKeyUsage: TLS Web Server Authentication
│   ├── AIA (Authority Information Access): OCSP URL
│   ├── CRLDistributionPoints: CRL URL
│   └── SCT (Signed Certificate Timestamps): CT 로그 증거
└── Signature (발급자의 서명)

7.2 Chain of Trust

Root CA (자체 서명, 브라우저/OS에 내장)
   |
   | 서명
   v
Intermediate CA
   |
   | 서명
   v
End-entity (example.com 인증서)

클라이언트는 체인을 따라가며 검증. 각 링크에서 "발급자의 공개키로 서명 검증 + 발급자가 CA 권한을 가졌는지". 최종적으로 root가 OS/브라우저에 내장된 신뢰 목록에 있어야 OK.

7.3 Revocation — 취소된 인증서

인증서 탈취되면 빨리 취소해야 한다. 두 가지 방법:

CRL (Certificate Revocation List): CA가 취소된 인증서 serial 목록 공개. 클라이언트가 주기적 다운로드. 크고 느림, 실시간성 부족.

OCSP (Online Certificate Status Protocol): 클라이언트가 CA에 "이 인증서 유효?" 질의. 실시간성 좋지만 매 연결마다 추가 RTT, 프라이버시 누출.

OCSP Stapling: 서버가 미리 OCSP 응답을 받아 TLS 핸드셰이크에 "stapling" (첨부). 클라이언트는 OCSP 서버에 따로 질의할 필요 없음. 프라이버시도 보호.

Must-Staple: 인증서에 "이 인증서는 반드시 stapling으로만 써야 함"을 기록. stapling 없으면 브라우저가 거부.

7.4 Certificate Transparency (CT)

2013년 Google이 제안한 공개 감사 시스템.

효과: 악의적/실수 발급된 인증서가 공개됨. 도메인 소유자가 감시 가능.

7.5 Let's Encrypt와 ACME

2015년 시작. 무료, 자동화된 인증서 발급. ACME (Automatic Certificate Management Environment) 프로토콜로 발급/갱신.

1. 클라이언트가 도메인 소유권 증명 (HTTP-01 또는 DNS-01 challenge).
2. ACME 서버가 검증 후 인증서 발급.
3. 90일 유효, 자동 갱신.

2026년 현재 웹의 95% 이상이 Let's Encrypt (또는 ZeroSSL, Buypass 등 ACME 호환) 인증서. 유료 CA는 거의 EV (Extended Validation) 등 특수 용도만 남음.


8. Session Resumption — 재연결의 예술

8.1 왜 재개가 필요한가

풀 핸드셰이크는 비싸다: ECDHE 연산, 인증서 검증, 전체 왕복. 같은 서버에 수초 안에 재연결하면서 매번 풀 핸드셰이크면 낭비.

TLS 1.3의 PSK (Pre-Shared Key) Resumption.

8.2 메커니즘

첫 연결 끝에 서버가 NewSessionTicket 메시지 전송. 내용:

클라이언트가 재연결 시:

장점: 인증서 검증 스킵, ECDHE 스킵 가능 (PSK-only 모드), RTT 감소.

단점: 서버가 티켓 해독 키를 돌리므로 상태 있음. 또는 stateless ticket (encrypted session state 자체). 키 관리가 중요 — 티켓 키 유출 = 모든 세션 복호화.

8.3 Forward Secrecy와의 긴장

PSK-only는 forward secrecy 없음. 그래서 TLS 1.3은 PSK with ECDHE 모드를 기본 권장. 티켓으로 인증은 스킵, 그래도 ECDHE는 수행.

세션 티켓 키는 주기적 교체해야 한다 (일 단위). Nginx의 ssl_session_ticket_key, Envoy의 session_ticket_keys.

8.4 0-RTT — 재개의 극단

Resumption을 더 밀어붙여: 클라이언트가 첫 왕복 완료 전에 이미 application data 전송.

Client                                Server
  |                                     |
  | ClientHello                         |
  |   + early_data                      |
  |   + pre_shared_key                  |
  | [early application data] -------->|
  |                                     |
  | <-------------------- ServerHello  |
  |                      + key_share    |
  |                [EncryptedExtensions]|
  |                         [Finished]  |
  | [Finished] ------------------------>|
  |                                     |
  | [application data] <--------------->|

0-RTT. RTT가 0 — 요청이 핸드셰이크와 동시에 도착.

8.5 Replay 공격 — 0-RTT의 위험

0-RTT 데이터는 replay될 수 있다. 공격자가 합법적 0-RTT 요청을 캡처해 나중에 재전송하면 서버는 구분 못 함.

완화책:

Cloudflare, Google은 0-RTT를 GET에만 허용하며 replay 탐지 레이어를 쌓는다. 일반 애플리케이션은 0-RTT를 비활성화하는 것이 가장 안전.


9. ECH — Encrypted Client Hello

9.1 SNI의 프라이버시 누출

TLS 1.3도 ClientHello의 SNI(서버 이름)는 평문이다. ISP나 방화벽이 어느 사이트에 접속하는지 볼 수 있다.

9.2 ECH

2024년 본격 배포 시작. SNI를 포함한 "inner ClientHello"를 대칭 암호화 → "outer ClientHello" 속에 숨김.

Cloudflare, Firefox가 선도. 2026년 현재 실전 배포 진행 중.

9.3 영향


10. QUIC — 전송 계층의 재발명

10.1 왜 QUIC인가

TLS over TCP의 문제:

  1. Head-of-line blocking: TCP는 단일 순서 스트림. 한 패킷 잃으면 전체 대기. HTTP/2가 stream 다중화를 추가했지만 TCP 층에서는 여전히 HOL 존재.
  2. 연결 설정 RTT: TCP 3-way handshake + TLS 핸드셰이크. 최소 2 RTT.
  3. 연결 이전 불가: IP 변경(Wi-Fi → 셀룰러)이면 연결 리셋.
  4. OS 커널 락인: TCP는 커널 공간, 업그레이드 느림.

QUIC는 이 모든 걸 풀려고 2012년 Google이 시작. 2021년 RFC 9000으로 IETF 표준.

10.2 UDP 위의 전송 프로토콜

QUIC는 UDP 위에서 TCP-like 신뢰 전송 + TLS 1.3 + 멀티 스트림을 재구현.

HTTP/3
  |
 QUIC (TLS 1.3 내장, stream 다중화)
  |
 UDP
  |
 IP

왜 UDP? 새 transport 프로토콜을 방화벽/NAT이 차단 안 함. 커널 건드릴 필요 없이 유저스페이스 구현.

10.3 Long Header와 Short Header

QUIC 패킷은 두 종류:

Long Header (핸드셰이크):

+-+-+-+-+-+-+-+-+
|1|1|T T|R R|P P|    // T=packet type, R=reserved, P=packet number length
+-+-+-+-+-+-+-+-+
|   Version (32)    |
+-+-+-+-+-+-+-+-+
| DCID Len | DCID   |    // Destination Connection ID
+-+-+-+-+-+-+-+-+
| SCID Len | SCID   |    // Source Connection ID
+-+-+-+-+-+-+-+-+
| ... type-specific fields ... |
+-+-+-+-+-+-+-+-+

Short Header (1-RTT, 평상시):

+-+-+-+-+-+-+-+-+
|0|1|S|R|R|K|P P|    // S=spin bit, K=key phase
+-+-+-+-+-+-+-+-+
| DCID                |
+-+-+-+-+-+-+-+-+
| Packet Number (8-32 bit) |
+-+-+-+-+-+-+-+-+
| Protected Payload          |
+-+-+-+-+-+-+-+-+

핵심: Connection ID가 있어 IP/포트 무관하게 연결 식별. 이것이 connection migration의 기반.

10.4 1-RTT 핸드셰이크

QUIC는 TCP 3WHS + TLS 1.3을 결합해 1 RTT에 끝낸다:

Client                                Server
  |                                     |
  | Initial (ClientHello) ---------->  |
  |                                     |
  | <------- Initial (ServerHello)     |
  |           Handshake (EncryptedExt, |
  |             Cert, CertVerify,       |
  |             Finished)               |
  | Handshake (Finished) ----------->  |
  | [1-RTT data] ------------------->  |
  | <------------- [1-RTT data]        |
  |                                     |

TLS 1.3 메시지가 QUIC 패킷 안에 프레임으로 들어간다. QUIC 자체도 첫 Initial 패킷부터 이미 암호화 (공개 솔트 기반 "initial keys"로).

0-RTT도 QUIC에서 지원. 같은 replay 주의사항.

10.5 Streams — 진짜 멀티플렉싱

QUIC의 stream은 독립된 순서 전송. 한 stream의 패킷 유실이 다른 stream에 영향 없음.

Connection
├── Stream 0 (client-initiated, bidirectional)
├── Stream 4 (client-initiated, bidirectional)
├── Stream 8 (client-initiated, bidirectional)
├── Stream 3 (server-initiated, unidirectional)
...

Stream ID의 하위 2비트로 종류 구분:

HTTP/3는 각 요청/응답을 별도 stream에 매핑. 한 요청이 느려도 다른 요청이 방해 안 받음.

10.6 Frame — Payload 구조

QUIC 페이로드 내부는 frame들의 연속:

한 패킷에 여러 frame을 담을 수 있음. ACK + STREAM + PING을 한 번에.

10.7 Connection Migration

Wi-Fi에서 셀룰러로 전환 → IP 변경. TCP라면 연결 리셋. QUIC는:

  1. 클라이언트가 새 IP에서 PATH_CHALLENGE frame 전송.
  2. 서버가 PATH_RESPONSE로 응답.
  3. 양쪽이 새 경로 검증 → 기존 연결 계속.

Connection ID가 유지되므로 서버는 "같은 연결의 새 경로"로 인식. HTTP 세션, 상태 전부 유지.

모바일 사용 시 엄청난 UX 개선. 영상 스트리밍이 네트워크 전환에도 끊기지 않음.

10.8 Congestion Control

QUIC의 congestion control은 TCP와 동일한 알고리즘(CUBIC, BBR) 사용. 차이는:

Google은 2016년 BBR을 QUIC에 먼저 배포하고 후에 TCP에 이식.


11. HTTP/3 — QUIC 위의 HTTP

11.1 구조

HTTP/3는 본질적으로 HTTP/2의 frame 모델을 QUIC 위로 이식.

11.2 QPACK — 헤더 압축 재설계

HPACK의 문제: 동적 테이블 업데이트가 stream 간 의존성을 만듦. QUIC의 독립 stream과 궁합 안 좋음.

QPACK은 동적 테이블 업데이트를 별도 unidirectional stream으로 분리:

이 분리로 stream 간 HOL blocking 제거. 압축률은 비슷, 복잡도는 증가.

11.3 Alt-Svc — HTTP/3로의 업그레이드

클라이언트는 처음 HTTP/1.1 또는 HTTP/2로 접속. 서버가 Alt-Svc: h3=":443" 헤더로 "HTTP/3도 가능"을 알림. 브라우저가 다음 연결을 QUIC로 시도.

2024년 이후 DNS의 HTTPS record (RFC 9460)로 처음부터 HTTP/3 발견 가능:

example.com. HTTPS 1 . alpn=h3 ipv4hint=192.0.2.1

11.4 배포 현황

Cloudflare, Google, Facebook, Fastly가 선도. 2026년 현재 대형 CDN은 HTTP/3 기본 활성화.

자체 호스팅: Nginx 1.25+, Caddy 2+, Envoy, HAProxy 최신판이 지원. 리눅스 커널 6.0+의 io_uring + UDP GSO/GRO로 성능 대폭 향상.

11.5 성능 실측

조건에 따라 다르지만 대략:

단, QUIC은 CPU 사용량이 TCP+TLS보다 높다 (유저스페이스, UDP 패킷 개별 처리). GSO(Generic Segmentation Offload)와 BPF 최적화가 갭 축소 중.


12. 실전 배포

12.1 Nginx로 TLS 1.3 + HTTP/3

server {
    listen 443 ssl http2;
    listen 443 quic reuseport;       # HTTP/3 = QUIC
    listen [::]:443 ssl http2;
    listen [::]:443 quic reuseport;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
    ssl_prefer_server_ciphers off;   # 1.3에서는 불필요

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets on;

    ssl_stapling on;
    ssl_stapling_verify on;

    # HTTP/3 Alt-Svc 광고
    add_header Alt-Svc 'h3=":443"; ma=86400';

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        ...
    }
}

12.2 Cloudflare 이용 시

단순: SSL/TLS → Edge Certificates → Minimum TLS Version을 1.2 또는 1.3. "Always Use HTTPS", "HTTP/3", "0-RTT Connection Resumption" 토글 켜면 끝. 복잡한 건 전부 Cloudflare가 한다.

12.3 인증서 자동화

Let's Encrypt + Certbot 또는 acme.sh:

# Certbot
certbot --nginx -d example.com -d www.example.com

# 갱신은 systemd timer가 자동
systemctl list-timers | grep certbot

갱신 실패 모니터링 중요. 인증서 만료는 가장 흔한 장애 원인 중 하나.

12.4 보안 강화 체크리스트

- TLS 1.0/1.1 비활성 (1.2 이상만).
- 약한 cipher suite 제거 (RC4, DES, 3DES, MD5).
- OCSP stapling 활성.
- HSTS 활성 + preload.
- SAN에 wildcard + apex 모두 포함.
- CT monitor 설정 (내 도메인에 이상 발급 감시).
- 키 파일 권한 600, root 소유.
- 정기적 key rotation (연 단위).
- ssllabs.com/ssltest 에서 A+ 등급 확인.

12.5 디버깅 도구

# TLS 버전, cipher 확인
openssl s_client -connect example.com:443 -tls1_3

# 인증서 체인
openssl s_client -connect example.com:443 -showcerts

# TLS 1.3 핸드셰이크 추적
openssl s_client -connect example.com:443 -trace -tls1_3

# HTTP/3 테스트
curl --http3 https://example.com -v

Wireshark는 TLS 1.3 복호화도 지원 — Chrome/Firefox의 SSLKEYLOGFILE 환경변수로 세션 키 덤프 → Wireshark에 로드.

12.6 성능 측정

# 핸드셰이크 시간 측정
curl -w "%{time_connect} + %{time_appconnect} = %{time_pretransfer}\n" -o /dev/null -s https://example.com

# HTTP/3 강제
curl --http3-only https://example.com -w "%{time_total}\n"

RUM(Real User Monitoring)으로 실제 사용자 경험 측정이 최고. Cloudflare의 Web Analytics, Google의 Search Console CWV가 무료.


13. 보안의 최신 위협과 방어

13.1 Downgrade 공격 방어

공격자가 ClientHello를 변조해 TLS 1.0으로 강제. 방어:

13.2 Middlebox 문제

기업 방화벽/IDS가 TLS를 해독하려 "TLS intercept"을 한다. 사설 CA 설치 → 클라이언트가 신뢰 → 중간자 공격과 동일한 구조.

TLS 1.3이 이런 미들박스들에 의해 많이 방해받았다. GREASE(Generate Random Extensions And Sustain Extensibility) 메커니즘으로 미들박스의 가정을 깨서 strict 검증을 강제.

13.3 TOFU vs PKI

TOFU (Trust On First Use): 첫 연결 시 키를 저장, 이후 비교. SSH 모델.

PKI (Public Key Infrastructure): CA 체인으로 매번 검증. 웹의 모델.

PKI는 확장성이 좋지만 CA의 신뢰를 가정. CA가 해킹되면 전체 무너짐 (DigiNotar 2011).

TLS Certificate Pinning: 앱이 특정 인증서 해시를 저장. 바뀌면 거부. PKI + TOFU의 하이브리드. 모바일 앱에서 널리 쓰이다가 운영 복잡도로 HPKP(HTTP Public Key Pinning)은 deprecated. 대체로 Expect-CT 헤더.

13.4 Kyber — Post-Quantum 시대

2024년 NIST가 Kyber를 ML-KEM으로 표준화. 2025년부터 주요 브라우저/CDN이 X25519Kyber768 하이브리드 배포.

key_share.group = X25519Kyber768  (새로 할당된 ID)
key_share.key_exchange = X25519_pub || Kyber_pub  (concatenated)

shared secret은 둘을 HKDF로 결합. 양자 컴퓨터가 X25519를 깨도 Kyber가 지킴, 반대도 마찬가지.

단점: 공개키 크기 1184바이트. X25519의 32바이트와 큰 차이. MTU(Maximum Transmission Unit) 초과 시 핸드셰이크 패킷 분할 → 성능 영향. QUIC는 프로토콜 레벨에서 이미 분할 지원, TCP보다 유리.


14. 깊이 들어간 개발자를 위한 팁

14.1 OpenSSL에서 직접

#include <openssl/ssl.h>

SSL_CTX* ctx = SSL_CTX_new(TLS_method());
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
SSL_CTX_set_ciphersuites(ctx,
    "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256");

BoringSSL (Google), LibreSSL (OpenBSD), s2n-tls (AWS)가 OpenSSL 대안. 단순화된 API와 더 엄격한 보안 기본값이 매력.

14.2 Go 언어에서

import "crypto/tls"

config := &tls.Config{
    MinVersion: tls.VersionTLS12,
    CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
    CipherSuites: []uint16{
        tls.TLS_AES_128_GCM_SHA256,
        tls.TLS_AES_256_GCM_SHA384,
        tls.TLS_CHACHA20_POLY1305_SHA256,
    },
}

Go의 crypto/tls는 TLS 1.3을 깔끔히 지원. QUIC는 quic-go 라이브러리.

14.3 Rust에서 rustls

use rustls::{ClientConfig, RootCertStore};

let mut root_store = RootCertStore::empty();
root_store.add_parsable_certificates(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

let config = ClientConfig::builder()
    .with_safe_defaults()
    .with_root_certificates(root_store)
    .with_no_client_auth();

rustls는 OpenSSL 대안으로 빠르게 성장 중. Memory-safe, 더 작은 API, TLS 1.3 우선.

14.4 QUIC 구현 선택

용도에 맞게 선택. 프로덕션 로드가 있으면 quiche나 msquic, 학습용이면 quic-go.


맺음 — 한 바이트씩 이해하기

HTTPS는 더 이상 "인증서만 있으면 되는 것"이 아니다. 현대 TLS는:

각 레이어가 독립적 가치를 가지면서 함께 시너지를 낸다. 한 바이트씩 이해하면, 설정할 때도 장애 났을 때도 본질을 본다.

운영자로서 본능적으로 체크할 세 가지:

  1. TLS 1.3을 쓰는가. 1.2 이하만 지원하면 즉시 업그레이드. 성능과 보안 모두 양보 불가.
  2. 0-RTT를 제대로 다루는가. POST 절대 금지, GET만 허용, replay 탐지 배포.
  3. HTTP/3 옵션은 열었는가. 모바일 사용자 비중이 높으면 즉각적 UX 개선.

이 세 가지만 제대로면 전송 보안의 95%는 끝난다.

다음 글은 eBPF와 Cilium — 커널 프로그래밍으로 네트워크를 재발명하기를 다룬다. QUIC이 유저스페이스 네트워크 혁명이라면, eBPF는 커널스페이스 혁명이다. 둘이 만나면 어디로 가는가? 그 지점이 다음 글의 출발점이다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다