왜 암호화가 필요한가
이전 글에서 DNS가 도메인을 IP 주소로 변환하는 과정을 살펴봤습니다. DNS 질의가 끝나면 브라우저는 서버와 TCP 연결을 맺고 HTTP 요청을 보냅니다. 그런데 일반 HTTP는 평문 입니다. 네트워크상의 누구든 요청과 응답 내용을 볼 수 있습니다.
비밀번호, 카드 번호, 개인 정보가 평문으로 전송되면 큰 문제입니다. TLS (Transport Layer Security) 는 TCP 위에 암호화 계층을 추가해 이 문제를 해결합니다. HTTPS는 HTTP + TLS를 의미합니다.
대칭 암호화 vs 비대칭 암호화
TLS를 이해하려면 두 가지 암호화 방식을 먼저 알아야 합니다.
대칭 암호화(Symmetric Encryption) 는 암호화와 복호화에 같은 키를 사용합니다. AES-256-GCM이 대표적입니다. 속도가 빠르지만, 키를 어떻게 안전하게 전달할지가 문제입니다.
// 대칭 암호화 개념
const symmetricKey = "shared-secret-key";
const encrypted = AES.encrypt("Hello", symmetricKey);
const decrypted = AES.decrypt(encrypted, symmetricKey);
// decrypted === "Hello"
// 문제: 이 키를 상대방에게 어떻게 안전하게 전달하나?비대칭 암호화(Asymmetric Encryption) 는 공개키와 개인키 쌍을 사용합니다. 공개키로 암호화하면 개인키로만 복호화할 수 있고, 개인키로 서명하면 공개키로 검증할 수 있습니다. 암호화에는 RSA, ECIES가, 서명에는 RSA, ECDSA가 대표적입니다. 안전하지만 대칭 암호화보다 수백 배 느립니다.
// 비대칭 암호화 개념
const { publicKey, privateKey } = generateKeyPair();
// 공개키로 암호화 -> 개인키로만 복호화
const encrypted = RSA.encrypt("Hello", publicKey);
const decrypted = RSA.decrypt(encrypted, privateKey);
// decrypted === "Hello"
// 반대로, 개인키로 서명 -> 공개키로 검증 (디지털 서명)
const signature = RSA.sign("data", privateKey);
const isValid = RSA.verify("data", signature, publicKey);
// isValid === trueTLS는 두 방식을 조합 합니다. 비대칭 암호화로 안전하게 키를 교환하고, 이후 데이터 전송은 빠른 대칭 암호화를 사용합니다.
TLS 1.3 핸드셰이크 (1-RTT)
TLS 1.3 (RFC 8446) 은 핸드셰이크를 1-RTT (1 Round-Trip Time) 만에 완료합니다. TLS 1.2가 2-RTT 필요했던 것에 비해 지연이 줄었습니다.
아래 시각화에서 클라이언트와 서버 사이의 TLS 1.3 핸드셰이크를 단계별로 확인하세요.
TCP SYN TCP SYN-ACK TCP ACK
TCP 3-way handshake 완료 포트 443 연결 수립
핸드셰이크 핵심 포인트
-
ClientHello에서 바로 키 교환 시작 : TLS 1.3에서는
key_share확장으로 첫 메시지에 ECDHE 공개키를 포함합니다. TLS 1.2에서는 서버의 선택을 기다린 후에야 키 교환을 시작했습니다. -
ServerHello 이후 암호화 : 서버가 ECDHE 공개키를 보낸 직후부터 메시지가 암호화됩니다. 인증서와 Finished 메시지도 암호화된 상태로 전송됩니다.
-
cipher suite 간소화 : TLS 1.3은 안전하지 않은 알고리즘을 제거했습니다. 지원되는 cipher suite는 5개뿐입니다. CBC 모드, RC4, SHA-1, 정적 RSA 키 교환 등이 삭제되었습니다.
// TLS 1.3에서 허용되는 cipher suites (RFC 8446 Section 9.1)
const tls13CipherSuites = [
"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",
];
// TLS 1.2에서 제거된 것들
const removed = [
"RSA 키 교환", // PFS를 제공하지 않음
"CBC 모드 cipher", // BEAST, Lucky13 등 공격에 취약
"RC4", // 편향 공격에 취약
"SHA-1", // 충돌 공격 가능
"압축", // CRIME 공격에 취약
"재협상", // 복잡성과 공격 표면 증가
];인증서 체인 (Certificate Chain)
서버가 보낸 인증서가 진짜인지 어떻게 확인할까요? 인증서 체인 (Chain of Trust) 으로 검증합니다.
// 인증서 체인 구조
const certificateChain = {
// 1. 서버 인증서 (leaf)
server: {
subject: "example.com",
issuer: "R12", // 중간 CA가 서명
publicKey: "서버의 공개키",
validity: { from: "2026-01-01", to: "2026-06-30" },
signature: "중간 CA의 개인키로 서명됨",
},
// 2. 중간 CA 인증서 (intermediate)
intermediate: {
subject: "R12",
issuer: "ISRG Root X1", // 루트 CA가 서명
publicKey: "중간 CA의 공개키",
signature: "루트 CA의 개인키로 서명됨",
},
// 3. 루트 CA 인증서 (root), 브라우저/OS에 사전 설치됨
root: {
subject: "ISRG Root X1",
issuer: "ISRG Root X1", // 자체 서명 (self-signed)
publicKey: "루트 CA의 공개키",
// 브라우저/OS가 이 인증서를 "신뢰"한다고 사전에 등록함
},
};검증 과정은 아래에서 위로 올라갑니다.
- 서버 인증서 검증 : 중간 CA의 공개키로 서버 인증서의 서명을 검증합니다. 도메인 이름과 유효 기간도 확인합니다.
- 중간 CA 검증 : 루트 CA의 공개키로 중간 CA 인증서의 서명을 검증합니다.
- 루트 CA 확인 : 루트 CA가 브라우저/OS의 신뢰 저장소에 있는지 확인합니다. 있으면 전체 체인이 유효합니다.
루트 CA 인증서는 브라우저나 OS에 사전 설치 되어 있습니다. macOS는 키체인에, Windows는 인증서 저장소에, Firefox는 자체 저장소에 보관합니다.
완전 순방향 비밀성 (PFS)
PFS(Perfect Forward Secrecy) 는 서버의 장기 개인키가 유출되더라도 과거 통신을 복호화할 수 없도록 보장하는 속성입니다.
// PFS가 없는 경우 (정적 RSA 키 교환, TLS 1.2 이하)
const withoutPFS = {
step1: "클라이언트가 서버의 RSA 공개키로 pre-master secret 암호화",
step2: "서버가 RSA 개인키로 복호화",
risk: "서버 개인키가 유출되면, 녹화해둔 모든 과거 통신을 복호화 가능",
};
// PFS가 있는 경우 (ECDHE 키 교환, TLS 1.3 필수)
const withPFS = {
step1: "매 세션마다 새로운 임시 (ephemeral) ECDHE 키 쌍 생성",
step2: "양쪽의 임시 공개키로 공유 비밀 계산 (Diffie-Hellman)",
step3: "핸드셰이크 후 임시 개인키 폐기",
guarantee: "서버 장기 개인키가 유출되어도 과거 세션 키를 유도할 수 없음",
};TLS 1.3은 정적 RSA 키 교환을 완전히 제거 했습니다. 모든 키 교환이 ECDHE 또는 DHE 기반이므로 PFS가 기본 보장됩니다. 이것이 TLS 1.3의 가장 중요한 보안 개선 중 하나입니다.
0-RTT 재개 (Early Data)
TLS 1.3은 이전에 연결했던 서버와 0-RTT 로 재연결할 수 있습니다. 첫 메시지에 애플리케이션 데이터를 포함하므로 핸드셰이크 지연이 사실상 없습니다.
// 0-RTT 재개 과정
const zeroRTT = {
// 첫 번째 연결: 정상적인 1-RTT 핸드셰이크
firstConnection: {
result: "서버가 세션 티켓 (PSK) 발급",
},
// 두 번째 연결: 0-RTT
resumption: {
clientHello: {
psk_identity: "이전 세션 티켓",
early_data: "GET / HTTP/1.1 (암호화된 요청)",
// 첫 메시지에 요청 데이터까지 포함!
},
},
// 주의사항
risks: [
"재전송 공격 (replay attack) 가능",
"멱등한 (idempotent) 요청에만 사용해야 함",
"POST, PUT 등 상태 변경 요청에는 위험",
],
};0-RTT는 성능은 좋지만 재전송 공격에 취약 합니다. 공격자가 0-RTT 데이터를 캡처해 다시 보낼 수 있습니다. 따라서 GET처럼 멱등한 요청에만 사용해야 합니다.
실무에서의 TLS
Let's Encrypt
Let's Encrypt는 무료로 TLS 인증서를 발급하는 CA (Certificate Authority) 입니다. ACME 프로토콜로 인증서 발급과 갱신을 자동화합니다.
# certbot으로 인증서 발급 (Nginx 기준)
sudo certbot --nginx -d example.com -d www.example.com
# 인증서 자동 갱신 (cron 또는 systemd timer)
sudo certbot renewLet's Encrypt 인증서는 기본 90일 유효이며, 2025년부터는 약 6일짜리 단기 인증서도 일반 제공됩니다. 짧은 유효 기간은 의도적인 설계로, 자동 갱신을 강제하여 키 유출 시 피해 기간을 줄입니다.
HSTS (HTTP Strict Transport Security)
HSTS는 브라우저에게 "이 도메인은 항상 HTTPS로만 접속하라"고 지시하는 응답 헤더입니다.
// HSTS 헤더
const hstsHeader = {
header: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
// max-age: 2년간 HTTPS만 사용
// includeSubDomains: 서브도메인도 적용
// preload: 브라우저 사전 로드 목록에 등록 요청
};
// HSTS가 방지하는 공격
const sslStripping = {
attack: "중간자가 HTTPS를 HTTP로 다운그레이드",
without_hsts: "사용자가 http://로 접속하면 공격자가 평문으로 가로채기",
with_hsts: "브라우저가 자동으로 HTTPS로 변환, HTTP 접속 불가",
};Mixed Content
HTTPS 페이지에서 HTTP 리소스를 로드하면 mixed content 경고가 발생합니다.
<!-- HTTPS 페이지에서 -->
<!-- 차단됨 (active mixed content) -->
<script src="http://cdn.example.com/app.js"></script>
<!-- 경고 (passive mixed content, 일부 브라우저에서 차단) -->
<img src="http://cdn.example.com/logo.png" />
<!-- 올바른 방법 -->
<script src="https://cdn.example.com/app.js"></script>
<img src="https://cdn.example.com/logo.png" />
<!-- 프로토콜 상대 URL (현재는 HTTPS 사용 권장) -->
<script src="//cdn.example.com/app.js"></script>Active mixed content(스크립트, 스타일시트, iframe 등) 는 대부분의 브라우저에서 차단됩니다. 스크립트가 조작되면 페이지 전체가 위험하기 때문입니다. Passive mixed content (이미지, 동영상 등) 는 경고만 표시하거나 최신 브라우저에서는 자동으로 HTTPS 업그레이드를 시도합니다.
프론트엔드 개발자를 위한 체크리스트
const tlsChecklist = {
// 필수
https: "모든 페이지를 HTTPS로 서빙",
hsts: "HSTS 헤더 설정",
redirect: "HTTP -> HTTPS 301 리다이렉트",
mixedContent: "모든 리소스를 HTTPS로 로드",
// 권장
tlsVersion: "TLS 1.2 이상만 허용 (TLS 1.0/1.1 비활성화)",
certAutoRenew: "인증서 자동 갱신 설정",
caa: "CAA DNS 레코드로 인증서 발급 CA 제한",
// 확인 도구
tools: [
"SSL Labs (ssllabs.com/ssltest), 서버 TLS 설정 점검",
"Security Headers (securityheaders.com), 보안 헤더 점검",
"Chrome DevTools > Security 탭, 인증서 및 연결 정보",
],
};다음 단계
이 글에서는 TLS가 통신을 암호화하는 원리를 살펴봤습니다. 대칭/비대칭 암호화의 조합, TLS 1.3의 1-RTT 핸드셰이크, 인증서 체인, 완전 순방향 비밀성까지 다루었습니다. 다음 글에서는 TLS 위에서 동작하는 HTTP/2와 HTTP/3 의 구조를 다루겠습니다.