들어가며 — 매일 쓰지만 아무도 모르는 바이트들
당신은 지금 이 글을 HTTPS로 읽고 있다. 브라우저와 서버 사이에 TLS 핸드셰이크가 일어났고, 수백 바이트의 암호학적 춤이 오간 후 암호화된 채널이 열렸다. 이 과정은 몇 ms 만에 끝났고, 당신은 눈치채지 못했다.
이 "눈치채지 못함"이 TLS의 성공이다. 동시에 비극이기도 하다. 거의 모든 개발자가 HTTPS를 사용하지만, 극소수만이 내부에서 일어나는 일을 안다. 그래서:
- "왜 TLS 1.2보다 1.3이 빠른가?" — 대답 못 함.
- "0-RTT를 왜 조심해야 하는가?" — 대답 못 함.
- "Cloudflare는 왜 HTTP/3를 밀었는가?" — 대답 못 함.
- "QUIC는 왜 UDP 위에 있나?" — 대답 못 함.
이 글은 이 질문들을 바이트 수준에서 답한다. 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.
- TLS 1.0 (1999): BEAST 공격에 취약.
- TLS 1.1 (2006): BEAST 일부 대응, 거의 안 쓰임.
- TLS 1.2 (2008): AEAD 암호 도입, 10년간 지배.
- TLS 1.3 (2018): 핸드셰이크 대폭 축소, 취약 알고리즘 제거.
2020년 이후 TLS 1.0/1.1은 브라우저에서 제거됐다. 이제는 1.2와 1.3만 남았고, 1.3이 빠르게 1.2를 밀어내고 있다. Cloudflare 통계(2026년): 트래픽의 80% 이상이 TLS 1.3.
1.2 TLS가 제공하는 것
네 가지 속성:
- Confidentiality: 중간자가 내용을 못 읽음 (대칭 암호화).
- Integrity: 전송 중 위변조 감지 (AEAD MAC).
- Authentication: 서버(선택적으로 클라이언트)의 신원 확인 (인증서 + 서명).
- Forward Secrecy: 서버 개인키가 나중에 유출돼도 과거 세션 복호화 불가 (ECDHE).
이 네 가지가 모두 만족되는 구간을 secure channel이라 한다. TLS 1.3은 이 네 가지를 이전 버전보다 엄격히 보장한다.
1.3 핵심 개념 정리
- 대칭 암호 (symmetric): 같은 키로 암호화/복호화. 빠름. AES-GCM, ChaCha20-Poly1305.
- 비대칭 암호 (asymmetric, public-key): 키 쌍. 공개키로 암호화 → 개인키로 복호화. 또는 개인키로 서명 → 공개키로 검증. RSA, ECDSA, Ed25519.
- 키 교환 (key exchange): 양쪽이 공유 비밀을 안전히 합의하는 프로토콜. ECDHE가 현대 표준.
- AEAD (Authenticated Encryption with Associated Data): 암호화 + 무결성을 한 번에. AES-GCM이 대표.
- KDF (Key Derivation Function): 비밀 값에서 여러 키를 유도. HKDF가 표준.
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의 문제들
- 협상 가능한 알고리즘이 너무 많음: ClientHello가 수십 개 cipher suite를 제안. 서버가 고름. 잘못된 선택이 취약점으로 이어짐 (Logjam, FREAK).
- 정적 RSA 키 교환이 허용됨: Forward Secrecy 없음.
- MAC-then-Encrypt: POODLE, Lucky13 공격의 원인.
- Padding oracle: CBC 모드의 구조적 약점.
- 핸드셰이크 메시지 평문: 중간자가 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). 브라켓 {}은 암호화된 메시지를, 대괄호 []는 완전히 암호화된 데이터를 의미한다.
주목할 점:
- ClientHello가 key_share를 이미 포함. 서버의 응답을 기다리지 않고 바로 키 교환 재료를 보냄.
- ServerHello 후 모든 핸드셰이크 메시지가 암호화됨. 인증서도 암호화. 중간자가 못 봄.
- 클라이언트는 서버의 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:
- supported_versions: 클라이언트가 지원하는 TLS 버전 목록. 1.3이 여기 있으면 "나는 1.3 가능".
- key_share: 클라이언트가 제안하는 (key_exchange_group, client_public_key) 쌍들. 보통 X25519와 secp256r1.
- signature_algorithms: 인증서 서명 알고리즘 목록. ECDSA P-256, Ed25519, RSA-PSS 등.
- server_name (SNI): 접속하려는 호스트 이름. TLS 1.3은 이 필드도 암호화하는 ECH (Encrypted Client Hello)로 발전 중.
- alpn: HTTP/1.1, HTTP/2, HTTP/3 등 application protocol 협상.
- psk_key_exchange_modes, pre_shared_key: 세션 재개용.
3.3 ServerHello와 그 뒤
서버가 받으면:
- Cipher suite 선택: ClientHello의 목록에서 하나 고름.
- Key share 선택: client_public_key와 자신의 private_key로 shared secret 계산.
- ServerHello 전송 (평문): 선택된 cipher suite, 자신의 key_share 포함.
- 이 시점 이후 handshake 트래픽 키로 암호화된 메시지 전송:
EncryptedExtensions: 나머지 extensions.Certificate: 서버 인증서 체인.CertificateVerify: 인증서 개인키로 지금까지의 핸드셰이크 transcript에 서명.Finished: handshake 완료 확인.
클라이언트는 받아서:
- 인증서 체인 검증 (CA 신뢰, 호스트명 일치, 만료 체크, revocation).
CertificateVerify의 서명을 인증서 공개키로 검증 — 서버가 인증서 개인키를 실제로 소유하는지 증명.- 자신의
Finished전송. - 이제 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)에서 같은 계산. 동일 보안 강도에 키 크기가 훨씬 작음.
- 3072-bit RSA ≈ 256-bit ECC (보안 강도 비교).
- DH 2048-bit보다 X25519(255-bit)가 더 안전하고 더 빠름.
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 표준에 명시.
장점:
- 매우 빠름 (CPU 명령어 수준 최적화).
- 작음 (32바이트 공개키).
- 단순한 API (32바이트 → 32바이트 → 32바이트 shared).
- Side-channel 저항성.
- 애매한 파라미터 선택이 없음 — NSA 백도어 의혹 없음.
구현은 매우 간결. OpenSSL, BoringSSL, Go crypto/ecdh 모두 X25519를 기본 권장.
ClientHello의 key_share에 X25519가 있으면 거의 항상 선택됨. 선택지: X25519, secp256r1 (NIST P-256), secp384r1.
4.5 Post-Quantum — 미래
양자 컴퓨터가 ECDHE를 깬다. 대비로 post-quantum 키 교환 연구 진행.
- Kyber (ML-KEM으로 표준화): 2024년 NIST 표준.
- X25519+Kyber 하이브리드: Cloudflare, Google이 2023년부터 배포.
2026년 현재는 하이브리드 모드가 실전 배포. 고전 보안과 양자 저항을 동시에.
5. HKDF — 키 스케줄
5.1 왜 여러 키가 필요한가
"shared secret"은 하나지만, TLS는 여러 키가 필요하다:
- handshake traffic key (핸드셰이크 암호화용)
- application traffic key (실제 데이터용, 별개 for client → server와 server → client)
- exporter key (EAP 등 외부용)
- resumption master key (다음 세션 재개용)
각각을 shared secret에서 파생해야 한다. 이 역할을 **HKDF (HMAC-based Key Derivation Function)**가 한다.
5.2 HKDF의 구조
두 단계:
- Extract: 불규칙한 입력(shared secret)을 "깨끗한" 의사난수로.
PRK = HMAC(salt, IKM). - 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"에서 다시 key와 iv가 파생된다:
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 조합은 여러 공격의 원인이었다:
- BEAST (2011): CBC IV 재사용.
- Lucky13 (2013): HMAC timing leak.
- POODLE (2014): padding oracle.
문제의 근본: 암호화와 무결성이 따로 수행됨 → 상호작용의 복잡성 → 버그.
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
실전에서는 세 개가 주류:
- AES-128-GCM: 하드웨어 AES-NI 가속 있으면 최고속.
- AES-256-GCM: 256-bit 키로 장기 보안 강화.
- ChaCha20-Poly1305: AES-NI 없는 환경(모바일 ARM 구세대)에서 빠름. Google이 밀었음.
최신 서버는 세 개 모두 지원. 클라이언트의 선호도에 따라 서버가 결정.
6.3 GCM의 구조
AES-GCM = AES-CTR + GHASH.
- CTR (counter) 모드: 카운터 값을 AES로 암호화 → 스트림 생성 → 평문과 XOR. 블록 크기(16B) 의존 없이 임의 길이 평문 처리.
- 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이 제안한 공개 감사 시스템.
- 모든 새 인증서는 공개 CT 로그에 기록되어야 함.
- 로그는 append-only Merkle tree.
- 인증서에 SCT (Signed Certificate Timestamp) 추가 — "이 인증서가 CT 로그에 들어갔다"의 증거.
- 브라우저는 SCT 없는 인증서(2018년 이후 발급) 거부.
효과: 악의적/실수 발급된 인증서가 공개됨. 도메인 소유자가 감시 가능.
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 메시지 전송. 내용:
- 티켓(서버가 상태를 기억할 수 있는 불투명 데이터).
- resumption 키를 파생할 nonce.
- 티켓 lifetime (기본 7일, 최대).
클라이언트가 재연결 시:
- ClientHello에
pre_shared_keyextension으로 티켓 제시. - 서버가 티켓 해독 → 과거 세션 식별 → PSK 복구.
- PSK를 현재 shared secret에 혼합 (또는 대체).
- 인증서 교환 생략 가능.
장점: 인증서 검증 스킵, 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 요청을 캡처해 나중에 재전송하면 서버는 구분 못 함.
완화책:
- 멱등적(idempotent) 연산에만 허용: GET은 OK, POST는 금지.
- 서버 측 replay cache: 최근 본 early_data의 PSK binder 해시를 기록. 중복 거부.
- Early data limit: 티켓마다 최대 early_data 바이트 수 제한.
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" 속에 숨김.
- 클라이언트가 DNS의
HTTPSrecord에서 ECH 공개키(HPKE) 획득. - Inner ClientHello를 HPKE로 암호화.
- Outer ClientHello는 공용 서버 이름(예:
cloudflare.com)으로 SNI 설정. - 서버가 받아서 HPKE 복호화 → inner의 실제 SNI 확인 → 해당 사이트로 라우팅.
Cloudflare, Firefox가 선도. 2026년 현재 실전 배포 진행 중.
9.3 영향
- ISP가 어느 사이트인지 모름. DNS도 암호화(DoH/DoT)했다면 완전 감추기.
- 국가 단위 SNI 검열 우회.
- 중간 장비가 호스트 기반 라우팅 불가능.
10. QUIC — 전송 계층의 재발명
10.1 왜 QUIC인가
TLS over TCP의 문제:
- Head-of-line blocking: TCP는 단일 순서 스트림. 한 패킷 잃으면 전체 대기. HTTP/2가 stream 다중화를 추가했지만 TCP 층에서는 여전히 HOL 존재.
- 연결 설정 RTT: TCP 3-way handshake + TLS 핸드셰이크. 최소 2 RTT.
- 연결 이전 불가: IP 변경(Wi-Fi → 셀룰러)이면 연결 리셋.
- 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비트로 종류 구분:
0b00: client-bidirectional0b01: server-bidirectional0b10: client-unidirectional0b11: server-unidirectional
HTTP/3는 각 요청/응답을 별도 stream에 매핑. 한 요청이 느려도 다른 요청이 방해 안 받음.
10.6 Frame — Payload 구조
QUIC 페이로드 내부는 frame들의 연속:
- STREAM: stream 데이터.
- ACK: 수신한 패킷 번호 확인.
- CRYPTO: TLS 메시지.
- MAX_DATA, MAX_STREAM_DATA: flow control.
- RESET_STREAM, STOP_SENDING: stream 종료.
- PING, NEW_CONNECTION_ID, PATH_CHALLENGE, PATH_RESPONSE: 연결 관리.
- CONNECTION_CLOSE: 전체 연결 종료.
한 패킷에 여러 frame을 담을 수 있음. ACK + STREAM + PING을 한 번에.
10.7 Connection Migration
Wi-Fi에서 셀룰러로 전환 → IP 변경. TCP라면 연결 리셋. QUIC는:
- 클라이언트가 새 IP에서
PATH_CHALLENGEframe 전송. - 서버가
PATH_RESPONSE로 응답. - 양쪽이 새 경로 검증 → 기존 연결 계속.
Connection ID가 유지되므로 서버는 "같은 연결의 새 경로"로 인식. HTTP 세션, 상태 전부 유지.
모바일 사용 시 엄청난 UX 개선. 영상 스트리밍이 네트워크 전환에도 끊기지 않음.
10.8 Congestion Control
QUIC의 congestion control은 TCP와 동일한 알고리즘(CUBIC, BBR) 사용. 차이는:
- 유저스페이스 구현 → BBR, BBRv2 등 최신 알고리즘 빠른 배포.
- Packet number가 stream과 분리 → ACK의 모호성(spurious retransmission 오탐) 제거.
- 더 정확한 RTT 측정.
Google은 2016년 BBR을 QUIC에 먼저 배포하고 후에 TCP에 이식.
11. HTTP/3 — QUIC 위의 HTTP
11.1 구조
HTTP/3는 본질적으로 HTTP/2의 frame 모델을 QUIC 위로 이식.
- TCP 대신 QUIC.
- 각 요청/응답 = QUIC stream 하나.
- 헤더 압축은 HPACK(HTTP/2용) 대신 QPACK 사용.
11.2 QPACK — 헤더 압축 재설계
HPACK의 문제: 동적 테이블 업데이트가 stream 간 의존성을 만듦. QUIC의 독립 stream과 궁합 안 좋음.
QPACK은 동적 테이블 업데이트를 별도 unidirectional stream으로 분리:
- Encoder stream: 동적 테이블 삽입.
- Decoder stream: 확인 응답.
- Request 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 성능 실측
조건에 따라 다르지만 대략:
- 좋은 네트워크: HTTP/2와 비슷 또는 약간 빠름.
- 패킷 유실 있는 네트워크: HTTP/3 압승 (stream 독립성).
- 모바일/Wi-Fi: HTTP/3 압승 (connection migration).
- 초기 연결: HTTP/3의 1-RTT 우위.
단, 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으로 강제. 방어:
Finished메시지가 지금까지의 transcript를 해시 → 변조 시 검증 실패.- TLS 1.3의 ServerHello random의 마지막 8바이트가 sentinel 값 → 클라이언트가 다운그레이드 탐지.
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 (Cloudflare, Rust): 성숙, Nginx 통합.
- quic-go (lucas-clemente, Go): HTTP/3 레퍼런스 구현.
- msquic (Microsoft, C): Windows/크로스 플랫폼.
- ngtcp2 (nghttp2 저자, C): 가볍고 embedded 친화적.
- s2n-quic (AWS, Rust): 보안 중심.
용도에 맞게 선택. 프로덕션 로드가 있으면 quiche나 msquic, 학습용이면 quic-go.
맺음 — 한 바이트씩 이해하기
HTTPS는 더 이상 "인증서만 있으면 되는 것"이 아니다. 현대 TLS는:
- TLS 1.3으로 핸드셰이크를 1-RTT로 축소.
- **ECDHE (X25519)**로 완벽한 forward secrecy.
- **AEAD (AES-GCM, ChaCha20-Poly1305)**로 암호화+무결성 통합.
- Session Resumption + 0-RTT로 재연결 가속.
- OCSP Stapling + CT로 인증서 생태계 감시.
- ECH로 SNI 암호화.
- QUIC + HTTP/3로 transport 자체를 재발명.
- Post-quantum hybrid로 미래 대비.
각 레이어가 독립적 가치를 가지면서 함께 시너지를 낸다. 한 바이트씩 이해하면, 설정할 때도 장애 났을 때도 본질을 본다.
운영자로서 본능적으로 체크할 세 가지:
- TLS 1.3을 쓰는가. 1.2 이하만 지원하면 즉시 업그레이드. 성능과 보안 모두 양보 불가.
- 0-RTT를 제대로 다루는가. POST 절대 금지, GET만 허용, replay 탐지 배포.
- HTTP/3 옵션은 열었는가. 모바일 사용자 비중이 높으면 즉각적 UX 개선.
이 세 가지만 제대로면 전송 보안의 95%는 끝난다.
다음 글은 eBPF와 Cilium — 커널 프로그래밍으로 네트워크를 재발명하기를 다룬다. QUIC이 유저스페이스 네트워크 혁명이라면, eBPF는 커널스페이스 혁명이다. 둘이 만나면 어디로 가는가? 그 지점이 다음 글의 출발점이다.
현재 단락 (1/492)
당신은 지금 이 글을 HTTPS로 읽고 있다. 브라우저와 서버 사이에 TLS 핸드셰이크가 일어났고, 수백 바이트의 암호학적 춤이 오간 후 암호화된 채널이 열렸다. 이 과정은 몇 m...