콘텐츠로 이동

TLS & HTTPS

TLS & HTTPS: 평문 HTTP에 신뢰를 붙이는 계층

섹션 제목: “TLS & HTTPS: 평문 HTTP에 신뢰를 붙이는 계층”

TLS(Transport Layer Security)는 두 엔드포인트 사이의 바이트 스트림에 기밀성, 무결성, 인증을 붙이는 보안 프로토콜이다. HTTPS(HTTP over TLS)는 HTTP 요청과 응답을 TLS가 만든 안전한 채널 위에서 주고받는 조합이다.

중요한 점은 “암호화됨”이 전부가 아니라는 것이다. 안전한 HTTPS 연결은 요청 본문을 숨기고, 중간 변조를 감지하며, 접속한 호스트가 기대한 서버인지 검증해야 한다.

퀴즈

HTTPS 자물쇠가 단순히 '내용이 암호화됨'보다 더 많은 의미를 갖는 이유는?

정답 보기

TLS는 HTTP 바이트를 암호화할 뿐 아니라 변조를 감지하고, 인증서 체인을 통해 서버 신원을 검증한다. 세 보장 중 하나가 빠지면 안전한 HTTPS 연결이라고 보기 어렵다.

이 문서는 L1 web-security-basics에서 배운 MITM(Man-in-the-Middle, 중간자 공격) 방어와 L2 tcp-udp-internals.mdx에서 배운 TCP 연결 수립 감각 위에서 시작한다. L1에서는 “HTTPS를 쓰면 도청과 변조를 막는다”까지 다뤘고, 여기서는 그 결론을 가능하게 만드는 핸드셰이크, 인증서 체인, 키 교환, 종료 위치, 운영 실패 신호를 학습한다.


2. 등장한 문제: HTTP는 왜 그대로 두면 위험한가

섹션 제목: “2. 등장한 문제: HTTP는 왜 그대로 두면 위험한가”

평문 HTTP는 요청과 응답을 그대로 네트워크에 흘려보낸다. 같은 Wi-Fi, 잘못된 프록시, 감염된 라우터, DNS 스푸핑 지점에 있는 공격자는 세 가지 일을 할 수 있다.

  1. 쿠키, Authorization 헤더, 개인정보를 읽는다.
  2. 응답 HTML이나 JSON을 바꾼다.
  3. 사용자를 가짜 서버로 유도한다.
사용자 브라우저 --HTTP 평문--> 공격자가 보는 네트워크 구간 --> 서버
|
| 읽기 가능: Cookie, Authorization, body
| 변조 가능: HTML, redirect, JSON field

TLS가 붙으면 이 그림은 이렇게 바뀐다.

사용자 브라우저 --TLS record--> 공격자가 보는 네트워크 구간 --> 서버
|
| 내용: 암호문
| 변조: AEAD 검증 실패
| 가짜 서버: 인증서 검증 실패

여기서 AEAD(Authenticated Encryption with Associated Data)는 암호화와 무결성 검증을 하나의 알고리즘 흐름에서 함께 제공하는 방식이다. 예를 들어 TLS 1.3의 TLS_AES_256_GCM_SHA384는 HTTP 바이트를 숨기면서, 중간자가 1바이트라도 바꾸면 수신자가 검증 실패로 연결을 버리게 만든다.

인증 보장을 이해하려면 세 단어를 먼저 잡아야 한다. X.509 인증서는 서버 공개키와 신원 정보를 담는 공개키 인증서 표준이고, CA(Certificate Authority, 인증기관)는 그 인증서에 서명해 “이 공개키를 이 주체의 것으로 믿어도 된다”고 위임하는 기관이다. SAN(Subject Alternative Name)은 인증서가 유효한 DNS 이름 목록이며, 브라우저와 대부분의 TLS 클라이언트는 접속한 호스트명이 이 목록에 들어 있는지 확인한다.

TLS가 붙이는 세 가지 보장

기밀성

HTTP 요청과 응답을 세션 키로 암호화해 중간 네트워크 장비가 내용을 읽지 못하게 한다.

쿠키, Authorization 헤더, 개인정보, 결제 정보가 네트워크를 지날 때

무결성

AEAD 또는 MAC 검증으로 전송 중 바이트가 바뀌었는지 확인하고, 검증 실패 시 데이터를 폐기한다.

응답 JSON, HTML, redirect, API 결과가 중간에서 바뀌면 안 될 때

인증

X.509 인증서 체인과 호스트명 검증으로 접속한 서버가 기대한 주체인지 판단한다.

공격자가 가짜 서버나 잘못된 인증서를 제시할 가능성을 차단할 때

세 보장은 서로 대체되지 않는다. 암호화만 있고 인증이 없으면 공격자가 가짜 서버와 암호화된 채널을 만들 수 있다. 인증만 있고 무결성이 없으면 중간자가 응답을 바꿔도 탐지할 수 없다. 무결성만 있고 기밀성이 없으면 바이트가 바뀌지는 않지만 내용은 그대로 읽힌다.

2-2. 세 보장이 각각 깨지는 반례

섹션 제목: “2-2. 세 보장이 각각 깨지는 반례”

같은 HTTPS 장애처럼 보여도 어떤 보장이 깨졌는지에 따라 원인과 대응이 달라진다.

깨진 보장작은 반례왜 위험한가관찰 신호
기밀성로그인 요청이 HTTP로 전송됨쿠키와 비밀번호가 네트워크에서 읽힘브라우저 “Not secure”, proxy capture에 평문 header 노출
무결성중간 프록시가 JSON 응답의 role을 바꿈클라이언트가 변조된 응답을 정상 데이터로 처리TLS가 없으면 탐지 불가, TLS record 검증 실패 시 연결 종료
인증공격자가 자체 서명 인증서를 제시사용자가 가짜 서버와 암호화된 채널을 맺음self signed certificate, unknown issuer
호스트명 검증api.example.com 접속에 www.example.com 인증서 제시같은 조직 도메인처럼 보여도 기대한 서버가 아님SAN mismatch, certificate subject name mismatch

이 표의 핵심은 “암호화된 가짜 서버”가 가능하다는 점이다. 공격자가 자기 서버와 TLS를 맺으면 바이트는 암호화된다. 하지만 CA 서명과 SAN 검증이 실패해야 하므로, 브라우저는 그 연결을 신뢰하지 않는다.

사용자가 다음 요청을 보낸다고 하자.

GET /me HTTP/1.1
Host: api.example.com
Cookie: session=abc123
Authorization: Bearer eyJ...

평문 HTTP에서는 같은 네트워크 구간의 관찰자가 session=abc123을 읽을 수 있다. TLS에서는 이 요청이 TLS Record Layer 안에서 암호문으로 바뀌고, 수신자는 AEAD tag를 검증한 뒤에야 HTTP 계층으로 넘긴다. 중간자가 /me/admin으로 바꾸거나 쿠키 값을 바꾸면 tag 검증이 실패한다.

이 예시가 중요한 이유는 TLS가 “HTTP를 더 똑똑하게 만드는 기능”이 아니라는 점이다. HTTP는 여전히 메서드, 헤더, 상태 코드로 동작하고, TLS는 그 바이트가 이동하는 채널의 읽기·변조·신원 문제를 해결한다.

2-4. 선행 기술의 한계: 패치가 아니라 계층이 필요했다

섹션 제목: “2-4. 선행 기술의 한계: 패치가 아니라 계층이 필요했다”

초기 웹은 평문 HTTP로도 충분했다. 정적 문서 중심이고, 쿠키와 로그인 세션이 오늘날처럼 광범위하게 쓰이지 않았기 때문이다. 하지만 로그인, 결제, 개인화 API가 늘어나면서 “네트워크가 선하다”는 전제가 깨졌다.

TLS의 lineage는 다음 한 문장으로 요약할 수 있다.

평문 HTTP는 도청·변조·가짜 서버를 구분하지 못한다
→ TLS가 암호화·무결성·인증을 한 채널에 묶는다
→ HTTPS는 HTTP 의미론을 유지하면서 안전한 전송 계층을 얻는다

RFC 2818은 HTTP를 TLS 위에서 실행하는 방식을 정의하면서, 기본 포트가 443이고 서버 인증서의 신원이 접속 호스트명과 일치해야 한다고 설명한다. 그래서 HTTPS는 단순히 http://https://로 바꾸는 문법이 아니라, “이 호스트와 안전한 채널을 만들었는가”라는 검증 절차다.


3. 핵심 철학: 느린 신뢰와 빠른 데이터 전송을 분리한다

섹션 제목: “3. 핵심 철학: 느린 신뢰와 빠른 데이터 전송을 분리한다”

TLS의 설계는 두 요구를 동시에 만족해야 한다.

  1. 처음 만나는 서버와 안전하게 키를 합의해야 한다.
  2. 합의가 끝난 뒤에는 대량의 HTTP 바이트를 빠르게 전송해야 한다.

그래서 TLS는 느리지만 신원과 키 합의에 필요한 비대칭키 연산을 핸드셰이크에 집중시키고, 실제 데이터 전송에는 빠른 대칭키를 쓴다.

대칭키 암호화는 같은 키로 암호화와 복호화를 수행한다. 빠르기 때문에 HTTP 본문, 헤더, 쿠키처럼 양이 많은 데이터를 처리하기에 적합하다. 문제는 통신 전에 그 키를 어떻게 안전하게 공유하느냐다.

비대칭키 암호화와 디지털 서명은 공개키와 개인키를 나누어 쓴다. 공개키는 노출되어도 되지만, 개인키는 서버만 가져야 한다. 이 방식은 대칭키보다 훨씬 비싸지만, 처음 만나는 상대와 키를 합의하거나 “이 공개키가 이 서버의 것”임을 증명하는 데 적합하다.

핸드셰이크 단계
- 인증서로 서버 신원 검증
- ECDHE로 세션 키 재료 합의
- 비대칭키와 디지털 서명이 중심
데이터 전송 단계
- 합의한 세션 키로 HTTP record 암호화
- AES-GCM 또는 ChaCha20-Poly1305 같은 AEAD가 중심

ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)는 타원곡선 기반의 임시 키 교환 방식이다. “Ephemeral”은 세션마다 새 임시 키를 만들고 버린다는 뜻이다. 이 성질 덕분에 서버 개인키가 나중에 유출되어도 과거 세션의 대칭키를 곧바로 복원할 수 없다.

3-2. 수치 감각: 왜 전송에는 대칭키를 쓰는가

섹션 제목: “3-2. 수치 감각: 왜 전송에는 대칭키를 쓰는가”

비대칭키 연산은 연결을 시작할 때는 감당할 수 있지만, 매 HTTP record마다 쓰기에는 비싸다. 대략적인 감각은 다음과 같다.

작업비용 감각어디에 쓰는가
ECDHE 키 교환연결당 1회세션 키 재료 합의
RSA/ECDSA 인증서 서명 검증연결당 1회 이상서버 인증서와 CertificateVerify 검증
AES-GCM/ChaCha20-Poly1305 암호화record마다 반복해도 빠름실제 HTTP 요청·응답 전송
X.509 체인·폐기 상태 확인연결 또는 캐시 주기마다 확인서버 신원과 인증서 상태 판단

정확한 숫자는 CPU, TLS 라이브러리, 키 크기에 따라 달라진다. 다만 같은 서버에서 대칭키 record 암호화는 대량 처리에 맞게 최적화되어 있고, 공개키 연산은 새 연결이 급증할 때 CPU 피크로 먼저 드러난다. 그래서 TLS 성능 문제를 볼 때는 요청 수보다 새 연결 수, handshake rate, session resumption 비율을 함께 본다.

여기서 X.509 인증서는 공개키 인증서의 표준 형식이다. 서버의 공개키, 주체 이름, 유효기간, SAN 같은 필드, 인증기관의 서명이 들어 있다. TLS 클라이언트는 이 구조를 읽어서 “이 공개키를 api.example.com 서버의 키로 믿어도 되는가”를 판단한다.

TLS를 읽을 때는 항상 세 질문으로 돌아오면 된다.

  1. 누구와 연결했는가? 인증서 체인, SAN, SNI가 답한다.
  2. 이 연결만의 키인가? ECDHE, TLS 1.3, session resumption 설정이 답한다.
  3. 어디서 암호가 풀리는가? TLS termination, ALB, Nginx, 서비스 메시가 답한다.

이 세 질문이 분리되면 운영 장애도 더 빨리 좁혀진다. 인증서 만료는 “누구와 연결했는가”의 실패이고, TLS 1.0 허용은 “이 연결만의 안전한 키인가”의 실패이며, ALB 뒤 백엔드 HTTP 구간은 “어디서 암호가 풀리는가”의 설계 결정이다.


4-1. 인증서 체인: 공개키를 어떻게 믿을 것인가

섹션 제목: “4-1. 인증서 체인: 공개키를 어떻게 믿을 것인가”

서버가 “이 공개키가 내 것”이라고 주장하는 것만으로는 부족하다. 공격자도 자기 공개키를 제시할 수 있기 때문이다. TLS는 인증기관(CA, Certificate Authority)이 서명한 X.509 인증서 체인으로 이 문제를 푼다.

Root CA
└── Intermediate CA
└── Leaf Certificate
└── api.example.com

Root CA는 운영체제나 브라우저의 trust store에 미리 들어 있는 최상위 신뢰 기준이다. Intermediate CA는 Root CA가 위임한 중간 인증기관이고, Leaf Certificate는 실제 서버가 제시하는 인증서다.

Root CA 개인키가 온라인에 노출되면 인터넷 전체 신뢰가 흔들린다. 그래서 Root CA는 보통 오프라인 또는 강한 HSM(Hardware Security Module)에 보관하고, 실제 발급 작업은 Intermediate CA가 담당한다. Intermediate CA가 침해되면 해당 중간 인증기관만 폐기하는 식으로 피해 범위를 줄일 수 있다.

SAN(Subject Alternative Name)은 인증서가 유효한 DNS 이름 목록이다. 현대 TLS 클라이언트는 오래된 CN(Common Name)보다 SAN을 기준으로 호스트명을 검증한다.

예를 들어 브라우저가 https://api.example.com에 접속하면 다음을 확인한다.

  1. Leaf 인증서가 Intermediate CA로 검증되는가?
  2. Intermediate CA가 신뢰 가능한 Root CA로 이어지는가?
  3. Root CA가 클라이언트 trust store에 있는가?
  4. Leaf 인증서의 SAN에 api.example.com이 있는가?
  5. 인증서 유효기간과 폐기 상태가 정상인가?
인증서 체인 검증 흐름
flowchart TD
A[서버가 Leaf와 Intermediate 인증서 전송] --> B{Leaf 서명이 Intermediate로 검증되는가?}
B -->|no| X[연결 실패]
B -->|yes| C{Intermediate 서명이 Root로 검증되는가?}
C -->|no| X
C -->|yes| D{Root CA가 trust store에 있는가?}
D -->|no| X
D -->|yes| E{SAN이 접속 호스트와 일치하는가?}
E -->|no| X
E -->|yes| F{유효기간과 폐기 상태가 정상인가?}
F -->|no| X
F -->|yes| G[HTTPS 연결 신뢰]

작은 반례를 보자. 인증서가 www.example.com에는 유효하지만 api.example.com SAN이 없다면, 같은 회사 도메인처럼 보여도 TLS 검증은 실패해야 한다. 사용자가 입력한 URL의 호스트와 인증서의 SAN이 맞지 않으면 “나는 어느 서버와 말하는가”라는 인증 보장이 깨진다.

4-3. 인증서 필드를 읽는 최소 예시

섹션 제목: “4-3. 인증서 필드를 읽는 최소 예시”

실제 인증서에서 처음 봐야 할 필드는 많지 않다.

Subject: CN=api.example.com
Issuer: CN=Amazon RSA 2048 M02, O=Amazon, C=US
Validity:
Not Before: Jan 1 00:00:00 2026 GMT
Not After : Apr 1 00:00:00 2026 GMT
Subject Alternative Name:
DNS:api.example.com
DNS:*.example.com
Public Key Algorithm: id-ecPublicKey
Signature Algorithm: ecdsa-with-SHA256

이 출력은 다음 질문으로 읽는다.

필드읽는 법실패하면 보이는 현상
Subject인증서 주체의 사람이 읽는 이름CN만 보고 판단하면 SAN 누락을 놓칠 수 있음
Issuer누가 이 인증서를 서명했는가신뢰 체인이 끊기면 issuer 관련 오류
Not After언제 만료되는가만료 후 certificate has expired
Subject Alternative Name어떤 호스트명에 유효한가hostname mismatch 또는 브라우저 경고
Public Key Algorithm서버 공개키 알고리즘구형 클라이언트 호환성과 성능 판단
Signature AlgorithmCA가 어떤 방식으로 인증서를 서명했는가약한 서명 알고리즘이면 정책상 거부 가능

여기서 와일드카드 *.example.comapi.example.com에는 맞지만 v1.api.example.com에는 보통 맞지 않는다. 와일드카드는 인증서 수를 줄이는 도구이지, 모든 하위 도메인을 무한히 덮는 신뢰 모델이 아니다.

worked example: 체인이 빠진 인증서 읽기

섹션 제목: “worked example: 체인이 빠진 인증서 읽기”

다음 두 출력은 둘 다 Leaf 인증서 자체는 정상처럼 보인다. 차이는 서버가 Intermediate CA를 함께 보내는지다.

정상 체인
0 s:CN=api.example.com
i:CN=R11, O=Let's Encrypt, C=US
1 s:CN=R11, O=Let's Encrypt, C=US
i:CN=ISRG Root X1, O=Internet Security Research Group, C=US
Verify return code: 0 (ok)
불완전한 체인
0 s:CN=api.example.com
i:CN=R11, O=Let's Encrypt, C=US
Verify return code: 20 (unable to get local issuer certificate)

첫 번째 출력은 Leaf의 issuer인 R11 인증서를 서버가 같이 보냈다. 클라이언트는 R11이 ISRG Root X1로 이어지는지 확인할 수 있다. 두 번째 출력은 Leaf만 있으므로, 클라이언트가 R11을 이미 알고 있지 않다면 체인을 완성할 수 없다.

이 예시는 cert.pemfullchain.pem의 차이를 설명한다. cert.pem은 Leaf만 담는 경우가 많고, fullchain.pem은 Leaf와 Intermediate를 함께 담는다. Nginx나 Apache에서 서버 인증서 파일을 고를 때 “내 인증서가 맞는가”뿐 아니라 “클라이언트가 Root까지 올라갈 길을 받는가”를 확인해야 한다.

worked example: SAN이 빠진 인증서 읽기

섹션 제목: “worked example: SAN이 빠진 인증서 읽기”

다음 인증서는 같은 회사 도메인을 담고 있어도 api.example.com 접속에는 실패한다.

Subject Alternative Name:
DNS:www.example.com
DNS:static.example.com
요청 URL:
https://api.example.com/me

브라우저는 “example.com 계열인가”를 추론하지 않는다. 접속한 호스트명 api.example.com이 SAN 목록에 정확히 들어 있는지 본다. 이 엄격함 덕분에 static.example.com을 운영하는 서버가 실수로 api.example.com 사용자 트래픽을 신뢰받는 일이 줄어든다.

4-4. TLS 1.2 핸드셰이크: 안전하지만 왕복이 많다

섹션 제목: “4-4. TLS 1.2 핸드셰이크: 안전하지만 왕복이 많다”

RTT(Round Trip Time)는 클라이언트가 패킷을 보내고 응답을 받는 데 걸리는 왕복 시간이다. TLS 1.2의 일반적인 풀 핸드셰이크는 실제 HTTP 요청 전 2 RTT가 필요하다. TCP 3-way handshake까지 포함하면 첫 요청은 지연을 더 크게 느낄 수 있다.

클라이언트 서버
|──── ClientHello ─────────────────────→ |
| 지원 버전, cipher suite, random_C |
| |
| ←── ServerHello ───────────────────── |
| ←── Certificate ───────────────────── |
| ←── ServerKeyExchange ─────────────── |
| ←── ServerHelloDone ───────────────── |
| |
|──── ClientKeyExchange ───────────────→ |
|──── ChangeCipherSpec ────────────────→ |
|──── Finished ────────────────────────→ |
| |
| ←── ChangeCipherSpec ─────────────── |
| ←── Finished ─────────────────────── |
| |
|════ 암호화된 HTTP 통신 시작 ══════════ |

TLS 1.2에서는 cipher suite 선택, 인증서 검증, 키 교환 파라미터 전달이 여러 메시지로 나뉘어 있다. 이 구조는 안전하게 동작할 수 있지만, 고지연 네트워크에서는 첫 요청의 체감 latency를 키운다.

세션 키는 다음 재료에서 파생된다.

premaster secret
+ client random
+ server random
-> master secret
-> client_write_key, server_write_key, MAC 또는 AEAD key

TLS 1.2에서도 ECDHE 기반 cipher suite를 쓰면 전방 보안이 가능하다. 하지만 TLS 1.2는 역사적으로 RSA key exchange, 오래된 CBC cipher suite, 구버전 프로토콜 fallback 같은 위험한 선택지가 함께 남아 있었다. 그래서 운영 정책에서는 TLS 1.0/1.1을 비활성화하고, TLS 1.2 이상만 허용하는 쪽으로 수렴한다.

4-5. TLS 1.3 핸드셰이크: 먼저 추측해서 왕복을 줄인다

섹션 제목: “4-5. TLS 1.3 핸드셰이크: 먼저 추측해서 왕복을 줄인다”

TLS 1.3은 ClientHello에 key_share를 미리 넣는다. 클라이언트가 “서버가 이 타원곡선 그룹을 지원할 것”이라고 추측하고 ECDHE 공개키를 먼저 보내므로, 서버는 첫 응답에서 바로 세션 키 재료를 계산할 수 있다.

TLS 1.3 1-RTT 핸드셰이크
sequenceDiagram
participant Client as Client
participant Server as Server

Client->>Server: ClientHello + key_share
Server-->>Client: ServerHello + key_share
Server-->>Client: EncryptedExtensions
Server-->>Client: Certificate + CertificateVerify
Server-->>Client: Finished
Client->>Server: Finished
Client->>Server: Encrypted HTTP request

TLS 1.3은 key_share를 먼저 보내 서버가 첫 응답에서 세션 키 재료를 계산하게 만든다.

TLS 1.3의 핵심 개선은 세 가지다.

  1. 풀 핸드셰이크가 보통 1 RTT로 줄어든다.
  2. ECDHE 기반 전방 보안이 사실상 기본이 된다.
  3. 취약한 오래된 cipher suite와 handshake 옵션을 제거한다.

TLS 1.2와 TLS 1.3의 학습 포인트

왕복 횟수

TLS 1.2는 보통 2 RTT가 필요하고, TLS 1.3은 ClientHello에 key_share를 넣어 보통 1 RTT로 줄인다.

첫 요청 latency가 사용자 경험이나 API timeout에 직접 영향을 줄 때

키 교환

TLS 1.3은 ECDHE 기반 전방 보안을 기본 전제로 삼고, 위험한 과거 key exchange 선택지를 줄인다.

서버 개인키 유출 시 과거 트래픽까지 복호화될 수 있는지 판단할 때

호환성

TLS 1.3이 더 안전하고 빠르지만, TLS 1.2 클라이언트가 남아 있으면 즉시 폐기하기 어렵다.

ALB나 Nginx 보안 정책을 TLS 1.3-only로 올릴지 결정할 때

worked example: RTT가 첫 요청에 미치는 영향

섹션 제목: “worked example: RTT가 첫 요청에 미치는 영향”

서울 사용자가 같은 리전의 edge에 접속해 RTT가 20ms라면 TLS 1.2와 TLS 1.3의 차이는 작게 느껴질 수 있다.

TCP handshake: 1 RTT = 20ms
TLS 1.2 handshake: 2 RTT = 40ms
첫 HTTP 요청 전 최소 지연: 약 60ms
TCP handshake: 1 RTT = 20ms
TLS 1.3 handshake: 1 RTT = 20ms
첫 HTTP 요청 전 최소 지연: 약 40ms

하지만 사용자가 먼 리전의 origin에 직접 붙어 RTT가 160ms라면 차이가 커진다.

TLS 1.2: TCP 160ms + TLS 320ms = HTTP 전 약 480ms
TLS 1.3: TCP 160ms + TLS 160ms = HTTP 전 약 320ms

이 계산은 서버 처리 시간, DNS, congestion, packet loss를 제외한 최소 감각이다. 실제 서비스에서 TLS 1.3, CDN edge 종료, session resumption을 쓰는 이유는 “암호화가 빠르다”가 아니라 첫 바이트를 받기 전의 왕복 비용을 줄이기 위해서다.

TLS 1.3이 항상 1 RTT인 것은 아니다. 클라이언트가 보낸 key_share 그룹을 서버가 지원하지 않으면 서버는 HelloRetryRequest로 다른 그룹을 요구한다. 그러면 클라이언트가 ClientHello를 다시 보내야 해서 2 RTT에 가까워진다.

ClientHello: key_share = x25519
Server: HelloRetryRequest, please use secp256r1
ClientHello: key_share = secp256r1
ServerHello ...

운영에서 이 반례가 자주 보이면 클라이언트와 서버의 supported groups 정책이 어긋난 것이다. TLS 1.3 자체가 느린 것이 아니라, 양쪽이 같은 빠른 경로를 고르지 못한 상태다.

운영 기본값은 보통 “TLS 1.3 우선, TLS 1.2 호환 유지, TLS 1.0/1.1 비활성화”다. TLS 1.2를 제거하기 전에는 7-14일 정도 실제 접속 로그에서 프로토콜 분포를 본다. 남은 TLS 1.2가 구형 브라우저인지, 파트너 API인지, 내부 배치인지에 따라 대응이 다르기 때문이다.

예시 로그 집계
TLSv1.3 184233
TLSv1.2 1287
TLSv1.0 3

이 예시에서 TLS 1.0은 즉시 차단 후보지만, TLS 1.2 1,287건은 먼저 출처를 분류해야 한다. “가장 높은 버전만 허용”은 보안상 단순해 보이지만, 자동화 클라이언트가 조용히 실패하고 재시도 큐에 쌓이는 silent failure를 만들 수 있다.

전방 보안(Forward Secrecy)은 서버 개인키가 나중에 유출되어도 과거 세션을 복호화하기 어렵게 만드는 성질이다.

전방 보안이 약한 모델
과거 트래픽 녹화
+ 나중에 서버 개인키 유출
-> 과거 세션 복호화 가능성이 커짐
ECDHE 기반 모델
세션마다 임시 키 쌍 생성
+ 세션 종료 후 임시 키 폐기
-> 서버 개인키만으로 과거 세션 키 복원이 어려움

TLS 1.3의 0-RTT(Zero Round Trip Time) early data는 재연결 클라이언트가 ClientHello와 함께 HTTP 요청 데이터를 바로 보내는 기능이다. 성능 이득은 크지만 replay attack 위험이 있다. 공격자가 early data를 다시 보내면 서버가 같은 요청을 두 번 처리할 수 있기 때문이다.

작은 반례는 결제 요청이다.

POST /payments HTTP/1.1
Idempotency-Key: 없음

이 요청이 0-RTT early data로 전송되고 재생되면 중복 결제가 발생할 수 있다. 반대로 정적 파일 GET은 부작용이 없고 캐시 가능한 경우가 많아 0-RTT 후보가 될 수 있다.

요청 유형0-RTT 판단이유
GET /assets/app.js조건부 허용 가능부작용이 없고 재생되어도 상태가 바뀌지 않음
HEAD /health허용 가능관찰용 요청이며 상태 변경이 없음
POST /payments금지재생되면 중복 결제 가능
PATCH /profile금지사용자 상태가 중복 변경될 수 있음
POST /searchAPI 의미를 따로 확인POST라도 실제 부작용이 없을 수 있으나 명시 필요

0-RTT 판단은 HTTP 메서드 이름만으로 끝나지 않는다. origin 서버가 그 경로의 부작용을 알고 있어야 하고, 게이트웨이는 위험한 early data를 425 Too Early로 재시도시키는 정책을 가질 수 있다.

퀴즈

0-RTT를 POST /payments에 켜면 안 되는 핵심 이유는?

힌트: 빠른 전송이 항상 안전한 전송은 아닙니다.

정답 보기

0-RTT early data는 재전송될 수 있으므로 상태 변경 요청이 중복 실행될 위험이 있다. 성능 이득보다 중복 결제 같은 정합성 손상이 더 크다.

4-7. SNI와 ALPN: 한 IP에서 인증서와 프로토콜을 고른다

섹션 제목: “4-7. SNI와 ALPN: 한 IP에서 인증서와 프로토콜을 고른다”

SNI(Server Name Indication)는 TLS ClientHello에 목적지 호스트명을 넣는 확장이다. 하나의 IP 주소나 로드밸런서가 여러 도메인을 처리할 때, 서버는 SNI를 보고 어떤 인증서를 제시할지 결정한다.

ClientHello
server_name: api.example.com
supported_versions: TLS 1.3
key_share: x25519 public key

SNI가 없으면 서버는 기본 인증서를 내보낼 수밖에 없다. 그래서 IP로 직접 HTTPS 접속을 시도하면 SAN 불일치가 발생하기 쉽다.

ALPN(Application-Layer Protocol Negotiation)은 TLS 핸드셰이크 안에서 HTTP/1.1, HTTP/2 같은 애플리케이션 프로토콜을 협상하는 확장이다. 다음 토픽에서 HTTP/2와 HTTP/3을 읽을 때, “브라우저와 서버가 어떤 HTTP 버전을 쓰기로 합의했는가”를 설명하는 연결고리가 된다.

ClientHello
server_name: www.example.com
application_layer_protocol_negotiation: h2, http/1.1
ServerHello / EncryptedExtensions
selected protocol: h2

SNI와 ALPN은 모두 “TLS가 HTTP를 모른다”는 직관을 조금 수정하게 만든다. TLS는 HTTP 본문 의미를 해석하지 않지만, 어떤 인증서를 쓸지와 어떤 상위 프로토콜로 넘길지는 핸드셰이크에서 협상한다.

4-8. mTLS: 클라이언트도 인증서를 제시한다

섹션 제목: “4-8. mTLS: 클라이언트도 인증서를 제시한다”

mTLS(mutual TLS, 상호 TLS 인증)는 서버만 인증서를 제시하는 기본 TLS와 달리, 클라이언트도 인증서를 제시해 양방향으로 신원을 확인하는 방식이다.

기본 TLS
클라이언트 -> 서버 인증서 검증
서버 -> 클라이언트는 보통 익명으로 둠
mTLS
클라이언트 -> 서버 인증서 검증
서버 -> 클라이언트 인증서 검증

서비스 간 통신에서는 “요청이 올바른 네트워크에서 왔다”만으로 부족하다. payment-service가 정말 회사가 발급한 workload identity를 가진 서비스인지 확인해야 한다. mTLS는 이 신원 확인을 애플리케이션 토큰보다 아래 계층에서 수행한다.

클라이언트 서버
|──── ClientHello ─────────────────────→ |
| ←── ServerHello + Certificate ─────── |
| ←── CertificateRequest ────────────── |
|──── Certificate (client cert) ───────→ |
|──── CertificateVerify ───────────────→ |
|──── Finished ────────────────────────→ |
| ←── Finished ─────────────────────── |

mTLS가 권한 부여를 자동으로 끝내지는 않는다. 클라이언트 인증서가 “payment-service”를 증명하더라도, 그 서비스가 /refunds를 호출할 권한이 있는지는 별도 정책이 필요하다. mTLS는 강한 인증 수단이고, authorization은 API 정책이나 서비스 메시 정책이 이어서 판단한다.

4-9. TLS Termination: 어디서 암호를 풀 것인가

섹션 제목: “4-9. TLS Termination: 어디서 암호를 풀 것인가”

TLS termination은 암호화된 HTTPS 트래픽을 복호화하는 지점이다.

클라이언트 --HTTPS--> [종료 지점] --HTTP 또는 HTTPS--> 백엔드
TLS termination

종료 지점은 보안, 관측, 성능, 인증서 운영을 모두 바꾼다.

종료 위치장점위험과 경계잘 맞는 상황
ALB(Application Load Balancer)ACM 자동 갱신, WAF 연동, HTTP 로그 관측 쉬움ALB 뒤 구간은 별도 설정 없으면 평문 HTTP일반적인 인터넷 공개 API
CloudFront 또는 CDN edge사용자와 가까운 곳에서 TLS 종료, 캐시 결합origin까지의 TLS와 Host 헤더 정책 확인 필요정적 자산, 글로벌 서비스
Nginx 또는 Envoy세밀한 TLS 정책, OCSP stapling 제어 가능인증서 파일과 키 관리 책임이 직접 생김자체 운영, 온프레미스, 세밀한 프록시 제어
종단간 TLS 또는 재암호화백엔드까지 암호화 유지인증서 수명주기와 디버깅 복잡도 증가규제, 내부망 불신, zero trust 지향
서비스 메시 sidecar서비스 간 mTLS 자동화mesh control plane 의존성 증가Kubernetes 내부 서비스 간 인증

중요한 반례가 있다. “ALB에서 HTTPS를 받았으니 내부도 항상 안전하다”는 말은 자동으로 참이 아니다. ALB가 TLS를 종료하고 Target Group으로 HTTP를 보내면, ALB 뒤 네트워크 구간은 평문이다. VPC 내부를 신뢰할 수 있는지, 규정상 종단간 암호화가 필요한지, 서비스 간 신원 확인이 필요한지를 따로 판단해야 한다.

worked example: 같은 요청이 종료 위치에 따라 어떻게 달라지는가

섹션 제목: “worked example: 같은 요청이 종료 위치에 따라 어떻게 달라지는가”

사용자가 https://api.example.com/orders를 호출한다고 하자. ALB에서 TLS를 종료하고 백엔드로 HTTP를 보내는 기본 구조는 다음과 같다.

Browser
-- HTTPS, TLS session A -->
ALB
-- HTTP, no TLS -->
ECS task

이 구조에서 브라우저는 ALB가 제시한 인증서를 검증한다. ALB는 HTTP 요청을 복호화했기 때문에 path, header, cookie를 보고 WAF나 listener rule을 적용할 수 있다. 백엔드는 X-Forwarded-Proto: https 같은 헤더로 원래 요청이 HTTPS였음을 알 수 있다.

종단간 재암호화를 쓰면 그림이 바뀐다.

Browser
-- HTTPS, TLS session A -->
ALB
-- HTTPS, TLS session B -->
ECS task

이 구조에서는 ALB 뒤 구간도 암호화된다. 대신 백엔드도 인증서를 가져야 하고, ALB가 백엔드 인증서를 어떻게 검증할지 정해야 한다. 두 TLS 세션은 서로 다른 세션이다. 브라우저가 백엔드 인증서를 직접 검증하는 것이 아니라, ALB와 백엔드 사이에 별도의 신뢰 관계가 생긴다.

NLB passthrough를 쓰면 또 다르다.

Browser
-- HTTPS, single TLS session -->
NLB
-- TCP passthrough -->
backend terminates TLS

이 구조에서는 백엔드가 클라이언트와 직접 TLS를 끝낸다. 네트워크 로드밸런서는 L4 수준에서 패킷을 넘기므로 HTTP header 기반 라우팅이나 WAF 적용이 제한된다. 그래서 “가장 안전한가”가 아니라 “어디에서 HTTP를 관측해야 하는가”와 함께 결정해야 한다.

신호의미먼저 확인할 것
백엔드가 redirect URL을 http://로 생성백엔드가 원래 scheme을 모름X-Forwarded-Proto, trusted proxy 설정
ALB health check는 성공하지만 브라우저 인증서 오류edge 인증서와 backend health는 다른 문제ALB listener 인증서, SNI, SAN
WAF rule이 기대처럼 동작하지 않음TLS passthrough로 HTTP를 보지 못할 수 있음종료 위치가 WAF 앞인지 뒤인지
내부 규정 감사에서 전송 암호화 지적ALB 뒤 HTTP 구간이 규정과 충돌재암호화, mTLS, service mesh
백엔드 로그에 client IP가 모두 ALB로 보임종료 지점에서 전달 헤더가 필요X-Forwarded-For, trusted hop 설정

4-10. 인증서 수명주기: 발급보다 갱신 실패가 더 위험하다

섹션 제목: “4-10. 인증서 수명주기: 발급보다 갱신 실패가 더 위험하다”

인증서는 언젠가 만료된다. 그래서 TLS 운영에서 더 중요한 질문은 “어떻게 발급하는가”보다 “누가 언제 갱신 실패를 알아차리는가”다.

AWS ACM(AWS Certificate Manager)은 AWS 로드밸런서, CloudFront 같은 서비스에 붙는 공개 인증서의 발급과 갱신을 관리한다. DNS 검증을 유지하면 자동 갱신 흐름을 기대할 수 있지만, 검증용 CNAME을 지우거나 인증서가 AWS 서비스에 연결되어 있지 않으면 갱신이 실패할 수 있다.

Let’s Encrypt는 공개 CA가 무료로 짧은 수명의 인증서를 발급하는 생태계다. 보통 certbot이나 cert-manager 같은 자동화 도구와 함께 쓴다. 유효기간이 짧기 때문에 수동 갱신을 전제로 두면 장애가 된다.

환경기본 선택지확인해야 할 실패 신호
ALB/CloudFrontACM 공개 인증서DNS 검증 레코드 누락, 만료 알림 미수신
EC2/Nginx 직접 노출Let’s Encrypt + 자동 갱신cron/systemd timer 실패, fullchain.pem 누락
Kubernetes Ingresscert-manager + ACME issuerSecret 갱신 실패, issuer challenge 실패
내부 서비스 간 mTLSPrivate CA, SPIFFE/SPIRE, mesh CAtrust bundle 불일치, 짧은 SVID 회전 실패
규제 환경의 종단간 TLSPrivate CA 또는 관리형 CA백엔드 인증서 만료, 체인 불완전

ACM 문서 기준으로 DNS 검증 인증서는 만료 전에 갱신 조건을 점검하고 실패 이벤트를 AWS Health/EventBridge로 전달한다. 운영 판단의 핵심은 “30일 전 알림 하나”보다, 검증 레코드와 연결 상태가 깨지는 시점에 신호를 받는 것이다.

worked example: 90일 인증서의 갱신 타임라인

섹션 제목: “worked example: 90일 인증서의 갱신 타임라인”

Let’s Encrypt 계열의 짧은 수명 인증서를 90일짜리로 받았다고 하자. 안전한 자동화는 대략 다음 사건을 만든다.

Day 0 인증서 발급, Nginx reload 성공
Day 55 자동 갱신 시도 시작
Day 60 갱신 성공, 새 fullchain 배치, reload 성공
Day 61 외부 probe가 새 Not After 확인
Day 75 아직 갱신 안 됐으면 warning
Day 83 아직 갱신 안 됐으면 critical
Day 90 만료, 사용자 장애

여기서 중요한 관찰은 certbot renew --dry-run 성공과 실제 서비스 성공이 다르다는 점이다. 갱신 명령이 성공해도 Nginx reload가 실패하면 프로세스는 예전 인증서를 계속 제공할 수 있다. 따라서 외부에서 notAfter를 읽는 probe가 필요하다.

ACM도 같은 원리다. 발급은 AWS가 관리하더라도 DNS 검증 CNAME이 사라지면 자동 갱신 조건이 깨질 수 있다. “관리형 인증서”는 “아무 것도 관찰하지 않아도 되는 인증서”가 아니다.

인증서 파일 이름이 만드는 사고

섹션 제목: “인증서 파일 이름이 만드는 사고”

Let’s Encrypt 계열 파일명은 처음 보면 헷갈린다.

파일보통 담긴 내용서버 설정에서의 의미
cert.pemLeaf 인증서단독 설정하면 chain 누락 위험
chain.pemIntermediate 인증서Leaf와 함께 제공되어야 함
fullchain.pemLeaf + IntermediateNginx ssl_certificate에 주로 사용
privkey.pem개인키외부 노출 금지, 권한 관리 필요

학습할 때는 fullchain.pemprivkey.pem의 짝을 기억하면 된다. 공개로 보내도 되는 것은 인증서 체인이고, 절대 노출되면 안 되는 것은 개인키다. 개인키가 유출되면 새 인증서를 발급하는 것만으로 끝나지 않고, 기존 인증서 폐기와 배포 경로 점검이 필요하다.

4-11. OCSP, session resumption, CT: 신뢰와 성능의 보조 장치

섹션 제목: “4-11. OCSP, session resumption, CT: 신뢰와 성능의 보조 장치”

OCSP(Online Certificate Status Protocol)는 인증서가 폐기되었는지 확인하는 프로토콜이다. 클라이언트가 CA의 OCSP 서버에 직접 물어보면 최신 상태를 알 수 있지만, 방문 사실이 CA에 노출되고 CA 응답 지연이 연결 지연으로 이어질 수 있다.

OCSP stapling은 서버가 CA의 OCSP 응답을 미리 받아 TLS 핸드셰이크에 붙여 보내는 방식이다. 클라이언트는 CA에 직접 요청하지 않고도 폐기 상태를 확인할 수 있다.

기본 OCSP
클라이언트 -> CA OCSP 서버에 직접 질의
OCSP stapling
서버가 OCSP 응답을 캐시
클라이언트에게 핸드셰이크 중 함께 전달

Session resumption(세션 재개)은 같은 클라이언트가 다시 접속할 때 풀 핸드셰이크 비용을 줄이는 기능이다. TLS 1.2 session ticket은 서버가 암호화한 세션 상태를 클라이언트에게 맡기고, TLS 1.3은 PSK(Pre-Shared Key) 기반 resumption을 쓴다. 성능에는 도움이 되지만 ticket key 회전과 0-RTT 정책을 함께 봐야 한다.

CT(Certificate Transparency)는 발급된 인증서를 공개 로그에 남기는 감사 시스템이다. CA가 잘못된 인증서를 발급해도 도메인 소유자가 CT log를 모니터링하면 이상 발급을 발견할 수 있다. CT는 “발급을 막는 장치”라기보다 “잘못된 발급을 공개적으로 감지하게 만드는 장치”에 가깝다.

세 보조 장치를 비교해서 기억하기

섹션 제목: “세 보조 장치를 비교해서 기억하기”
장치주된 질문좋아지는 것남는 경계
OCSP stapling이 인증서가 폐기되었는가?폐기 상태 확인의 latency와 privacy 부담 감소stapled response 만료, soft fail 정책
Session resumption다시 연결할 때 풀 핸드셰이크가 필요한가?재연결 latency와 CPU 비용 감소ticket key 관리, 0-RTT replay
CT이 도메인 인증서가 어디서 발급되었는가?잘못된 발급을 공개 로그로 감지발급 자체를 실시간으로 막지는 않음

작은 사고 실험을 해보자. 공격자가 어떤 CA 계정 탈취로 api.example.com 인증서를 발급받았다면 OCSP stapling은 바로 막아 주지 못한다. 그 인증서는 아직 폐기되지 않았기 때문이다. CT 모니터링은 “모르는 인증서가 발급됐다”는 신호를 줄 수 있고, 이후 도메인 소유자가 폐기를 요청하면 OCSP/CRL 계층이 그 상태를 배포한다.

반대로 서버의 session ticket key가 오래 회전되지 않는 문제는 CT가 잡아 주지 못한다. 인증서 발급의 문제가 아니라 세션 재개 비밀 관리의 문제이기 때문이다. 이처럼 TLS 보조 장치는 서로 겹쳐 보이지만, 감시하는 실패 면이 다르다.

4-12. 브라우저 경계: mixed content와 HSTS

섹션 제목: “4-12. 브라우저 경계: mixed content와 HSTS”

브라우저는 TLS 검증 결과를 사용자에게 자물쇠나 경고로 보여준다. 이 신호가 깨지는 흔한 이유는 인증서만이 아니다.

Mixed Content는 HTTPS 페이지가 HTTP 리소스를 불러올 때 발생한다. 예를 들어 HTML 문서는 TLS로 받았지만 이미지, 스크립트, API 호출 일부가 http://라면 그 하위 리소스는 평문 경로를 탄다.

<!-- HTTPS 페이지에서 위험한 하위 리소스 -->
<script src="http://cdn.example.com/app.js"></script>
<!-- 안전한 형태 -->
<script src="https://cdn.example.com/app.js"></script>

HSTS(HTTP Strict Transport Security)는 브라우저에게 “이 도메인은 앞으로 HTTPS로만 접속하라”고 기억시키는 응답 헤더다. 첫 방문 이후 사용자가 http://example.com을 입력해도 브라우저가 HTTPS로 바꿔 요청한다.

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

HSTS는 강력하지만 되돌리기 어렵다. includeSubDomainspreload를 적용하면 아직 HTTPS 준비가 안 된 하위 도메인까지 접속 불능이 될 수 있다. 처음에는 짧은 max-age로 검증하고, 모든 하위 도메인의 TLS 준비가 끝났을 때 확장하는 편이 안전하다.


TLS 버전 정책은 보안과 호환성의 균형이다. 단순히 가장 높은 버전만 허용하면 좋아 보이지만, 실제 서비스에는 내부 배치, 파트너 연동, 구형 SDK가 남아 있을 수 있다.

관찰 결과판단다음 행동
TLS 1.0/1.1 요청이 있음폐기 대상출처를 확인하고 차단 일정을 잡음
TLS 1.2 요청이 소수 남음즉시 차단보다 분류가 먼저User-Agent, IP, 경로, 파트너 여부 확인
TLS 1.3 대부분, TLS 1.2 없음TLS 1.3-only 후보내부 health check와 파트너 통합까지 재확인
handshake timeout 증가정책 강화가 클라이언트 실패를 만들었을 수 있음변경 시점과 프로토콜 분포를 비교

작은 수치 예시를 보자.

총 HTTPS 요청 1,000,000건 / 7일
TLS 1.3: 992,000
TLS 1.2: 7,950
TLS 1.0: 50

TLS 1.0 50건은 차단 후보지만, TLS 1.2 7,950건은 무시하기 어렵다. 이 중 7,000건이 결제 파트너 webhook이면 TLS 1.3-only 전환은 장애다. 반대로 모두 오래된 모니터링 봇이면 별도 교체 계획으로 해결할 수 있다.

인증서 키 알고리즘은 성능과 호환성에 영향을 준다. 수치는 환경마다 다르지만 방향성은 비교적 일관적이다.

알고리즘성능 감각인증서 크기 감각선택 기준
RSA 2048넓은 호환성, 더 큰 비용매우 오래된 클라이언트 호환성이 필요할 때
RSA 4096비용이 커짐더 큼일반 웹 서비스 기본값으로는 과한 경우가 많음
ECDSA P-256빠르고 작음작음현대 브라우저와 모바일 중심 서비스 기본 후보
Ed25519빠르고 작음작음TLS 생태계 호환성 확인 후 제한적으로 검토

Cloudflare의 ECDSA 성능 벤치마크와 Let’s Encrypt의 Android 호환성 사례가 보여주는 교훈은 같다. 더 현대적인 알고리즘은 성능에 유리하지만, 인증서 체인과 클라이언트 생태계 호환성을 함께 봐야 한다.

방향만 잡으면 된다. ECDSA 계열은 같은 보안 수준에서 인증서와 서명이 작고 검증 비용이 낮은 편이라 모바일과 대량 handshake에 유리하다. RSA는 호환성이 넓지만 키가 커질수록 handshake CPU와 전송 크기 부담이 커진다.

운영 기본값은 직접 고르는 알고리즘보다 인증서 발급자, CDN/로드밸런서, 지원해야 하는 클라이언트 정책이 먼저 정하는 경우가 많다. 따라서 이 표는 “ECDSA가 항상 정답”이라는 결론이 아니라, handshake CPU와 호환성 문제가 보일 때 어떤 축을 비교해야 하는지 알려주는 지도다.

다음 결정 트리는 운영에서 자주 쓰는 기준이다.

인터넷 공개 API인가?
├─ 예
│ ├─ AWS 로드밸런서를 쓰는가?
│ │ ├─ 예 -> ALB/CloudFront + ACM 종료를 기본값으로 검토
│ │ └─ 아니오 -> Nginx/Envoy + Let's Encrypt 자동 갱신 검토
│ └─ 규제상 백엔드까지 암호화가 필요한가?
│ ├─ 예 -> ALB 재암호화, NLB passthrough, 서비스 메시 검토
│ └─ 아니오 -> ALB 종료 + 내부망 경계와 보안그룹 확인
└─ 내부 서비스 간 호출인가?
├─ 서비스 신원 검증이 필요한가? -> mTLS 또는 service mesh
└─ 단순 내부 호출인가? -> 네트워크 경계와 앱 인증을 함께 검토

여기서 “기본값”은 정답이 아니라 출발점이다. 규제, 내부망 신뢰 수준, 트래픽 규모, 관측 필요성, 팀의 인증서 운영 역량이 선택을 바꾼다.

수동 갱신은 학습용 실습에서는 가능하지만 프로덕션에서는 위험한 기본값이다. 인증서 장애는 평소에는 조용하다가 만료일에 한 번에 사용자 전체 장애로 드러난다.

상황좋은 기본값피해야 할 패턴
AWS managed edgeACM + DNS 검증 + EventBridge 알림검증 CNAME을 임시 레코드처럼 취급
VM 또는 bare metal NginxLet’s Encrypt 자동 갱신 + reload 검증cert.pem만 설정하고 chain 누락
Kubernetes Ingresscert-manager + issuer 상태 모니터링Secret 만료일을 수동 캘린더로만 관리
내부 mTLS짧은 수명 인증서 + 자동 회전 + trust bundle 배포수년짜리 내부 인증서를 모든 서비스에 복사

갱신 자동화의 성공 기준은 “명령이 한 번 성공했다”가 아니다. 만료 45일, 30일, 7일 같은 시점에 실패를 볼 수 있고, 갱신 후 실제 리스너가 새 인증서를 제공하는지 확인할 수 있어야 한다.

mTLS는 강한 도구지만 모든 내부 API에 무조건 붙이면 운영 복잡도가 늘어난다.

조건mTLS 필요성이유
여러 팀의 서비스가 같은 클러스터에서 통신높음서비스 신원 경계가 필요
결제, 개인정보, 내부 관리자 API높음네트워크 내부자나 잘못된 라우팅 위험 감소
단일 서비스의 로컬 sidecar 통신낮거나 선택같은 host boundary라면 비용 대비 효과 확인
이미 API token/JWT가 있음대체가 아니라 보완으로 검토mTLS는 transport identity, token은 app identity
인증서 회전 자동화가 없음도입 전 자동화가 먼저수동 mTLS는 장애 표면이 커짐

반례는 “mTLS를 켰으니 인증 로직을 모두 제거해도 된다”는 생각이다. mTLS는 호출 주체의 workload identity를 증명하지만, 사용자 권한, 리소스 소유권, 행 단위 권한은 애플리케이션이 계속 판단해야 한다.

0-RTT는 latency를 줄이지만 replay 가능성을 남긴다. 처음에는 꺼 두고, 정말 필요한 경로만 제한적으로 켜는 편이 이해하기 쉽다.

0-RTT 후보
- 정적 asset GET
- 부작용 없는 health check
- 캐시 가능한 read endpoint
0-RTT 금지
- 결제, 주문, 환불
- 프로필 변경
- 세션 변경
- idempotency key 없는 POST

idempotency key가 있어도 0-RTT가 자동으로 안전해지는 것은 아니다. 서버가 중복 요청을 실제로 같은 결과로 접어야 하고, 여러 인스턴스 사이에서 같은 key를 일관되게 볼 수 있어야 한다.

실패 신호는 “같은 idempotency key가 서로 다른 인스턴스에서 각각 새 요청처럼 처리됐다”는 로그다. 0-RTT anti-replay 상태가 노드 로컬 메모리에만 있거나, edge와 origin의 정책이 다르면 재전송을 한 번만 처리한다는 보장이 깨진다.

시나리오 A: 작은 SaaS API를 AWS에 배포한다

섹션 제목: “시나리오 A: 작은 SaaS API를 AWS에 배포한다”

조건은 다음과 같다.

사용자 -> app.example.com
API -> api.example.com
인프라 -> Route 53, ALB, ECS
규제 -> 일반 개인정보, 강한 종단간 암호화 요구 없음
팀 역량 -> 인증서 수동 운영 경험 적음

이 경우 기본 선택은 ALB에서 TLS를 종료하고 ACM 인증서를 붙이는 것이다. 이유는 인증서 갱신 부담을 줄이고, ALB access log와 WAF에서 HTTP 정보를 볼 수 있으며, ECS task는 HTTP 서버로 단순하게 운영할 수 있기 때문이다.

하지만 이 결정에도 체크포인트가 있다.

  1. api.example.comapp.example.com이 인증서 SAN에 포함되는가?
  2. HTTP에서 HTTPS로 redirect가 일관되게 동작하는가?
  3. 백엔드가 X-Forwarded-Proto를 신뢰하도록 proxy 설정이 되어 있는가?
  4. ACM DNS 검증 CNAME을 Terraform이나 IaC에서 보존하는가?
  5. HSTS를 바로 preload로 올리지 않고 짧은 max-age부터 시작하는가?

시나리오 B: 결제 서비스와 주문 서비스가 내부에서 통신한다

섹션 제목: “시나리오 B: 결제 서비스와 주문 서비스가 내부에서 통신한다”

조건은 다음과 같다.

order-service -> payment-service
환경 -> Kubernetes
요구 -> 서비스 신원 확인, 내부자/오배포 방어
트래픽 -> 내부 east-west traffic

이 경우 mTLS 또는 서비스 메시를 검토할 수 있다. 목표는 “네트워크가 내부니까 믿는다”를 줄이고, 호출 주체가 회사가 발급한 workload identity를 가진 서비스인지 확인하는 것이다.

도입 전에 물어볼 질문은 다음과 같다.

  1. 인증서 또는 SVID가 자동 발급·회전되는가?
  2. trust bundle 배포가 실패하면 어떤 서비스가 멈추는가?
  3. mTLS identity를 authorization policy로 연결할 수 있는가?
  4. 장애 시 plain HTTP fallback을 허용할 것인가, 실패하게 둘 것인가?
  5. 로그에서 어떤 workload가 거부되었는지 볼 수 있는가?

시나리오 C: 온프레미스 Nginx가 공개 API를 직접 받는다

섹션 제목: “시나리오 C: 온프레미스 Nginx가 공개 API를 직접 받는다”

조건은 다음과 같다.

사용자 -> Nginx -> Node.js API
인증서 -> Let's Encrypt
운영 -> systemd timer 또는 cron으로 자동 갱신
제약 -> 관리형 ALB/ACM 없음

이 경우 핵심은 Nginx TLS 설정 자체보다 갱신 이후 reload와 외부 검증이다. certbot renew가 성공해도 Nginx가 새 파일을 읽지 못하면 사용자는 만료 인증서를 계속 받는다.

좋은 관찰 체인은 다음과 같다.

갱신 명령 성공
-> Nginx config test 성공
-> Nginx reload 성공
-> 외부 probe가 새 notAfter 확인
-> 만료 임계값 알림이 정상 상태로 돌아옴

이 시나리오에서 긴 명령을 외우는 것보다 중요한 것은 체인의 각 단계가 독립 실패할 수 있다는 점이다.


6-1. HTTPS는 애플리케이션 보안을 대신하지 않는다

섹션 제목: “6-1. HTTPS는 애플리케이션 보안을 대신하지 않는다”

HTTPS는 전송 중 도청과 변조를 막는다. 서버가 받은 뒤의 SQL Injection, XSS, 권한 체크 누락, 안전하지 않은 파일 업로드는 TLS가 해결하지 않는다.

HTTPS가 막는 것
- 네트워크 중간자의 쿠키 도청
- 응답 바이트 변조
- 가짜 서버 인증서
HTTPS가 막지 못하는 것
- 서버가 의도적으로 내려준 취약한 JavaScript
- DB에 저장된 평문 비밀번호
- 인증된 사용자의 권한 상승 버그

이 경계는 L1의 Defense in Depth와 이어진다. 채널이 안전해야 하지만, 채널만 안전하다고 앱이 안전해지는 것은 아니다.

6-2. 자물쇠 아이콘은 “모든 리소스가 안전하다”는 뜻이 아니다

섹션 제목: “6-2. 자물쇠 아이콘은 “모든 리소스가 안전하다”는 뜻이 아니다”

브라우저의 보안 표시가 정상이어도 애플리케이션 내부의 API 권한 버그는 남을 수 있다. 반대로 서버 인증서는 정상이어도 mixed content 하나 때문에 페이지 보안 표시가 깨질 수 있다.

자물쇠 신호를 읽을 때는 세 가지를 분리한다.

  1. 최상위 문서가 HTTPS로 로드되었는가?
  2. 인증서 체인과 SAN이 맞는가?
  3. 하위 리소스가 HTTP로 섞이지 않는가?

6-3. SNI는 암호화된 요청 본문이 아니다

섹션 제목: “6-3. SNI는 암호화된 요청 본문이 아니다”

SNI는 ClientHello에 들어가므로 전통적인 TLS에서는 관찰 가능한 메타데이터다. 즉 중간자는 HTTP path나 cookie는 읽지 못해도, 사용자가 어느 호스트명에 접속하려는지는 볼 수 있다.

이 경계는 CDN, 사내 프록시, 감시 회피 같은 주제에서 중요해진다. 다만 이 문서의 핵심은 SNI privacy가 아니라 “서버가 어떤 인증서를 선택하는가”다. 더 깊은 주제는 ECH(Encrypted ClientHello) 같은 별도 확장으로 넘어간다.

6-4. 인증서 체인 완전성은 클라이언트마다 다르게 드러난다

섹션 제목: “6-4. 인증서 체인 완전성은 클라이언트마다 다르게 드러난다”

Nginx에 cert.pem만 설정하고 fullchain.pem을 쓰지 않으면 어떤 최신 브라우저에서는 중간 인증서를 캐시로 보완해 통과할 수 있다. 그러나 구형 Android, 일부 IoT, curl, Java 클라이언트에서는 unable to get local issuer certificate로 실패할 수 있다.

이 반례는 “내 브라우저에서 열리니 정상”이라는 판단을 깨뜨린다. TLS는 다양한 클라이언트 trust store와 체인 빌딩 정책 위에서 검증된다.

6-5. HSTS preload는 강한 복구 방해 장치가 될 수 있다

섹션 제목: “6-5. HSTS preload는 강한 복구 방해 장치가 될 수 있다”

HSTS preload에 등록하면 주요 브라우저가 해당 도메인을 처음부터 HTTPS로만 접속한다. SSL strip 공격에는 강하지만, 잘못 등록하면 HTTPS 준비가 안 된 서브도메인도 HTTP fallback을 할 수 없다.

처음에는 다음 순서가 안전하다.

  1. max-age=300 같은 짧은 값으로 배포한다.
  2. 모든 하위 도메인의 HTTPS 상태를 확인한다.
  3. includeSubDomains를 적용한다.
  4. 충분히 운영한 뒤 preload를 검토한다.

6-6. OCSP는 항상 hard fail이 아니다

섹션 제목: “6-6. OCSP는 항상 hard fail이 아니다”

브라우저와 클라이언트는 OCSP 서버가 응답하지 않을 때 연결을 막을 수도 있고 통과시킬 수도 있다. 후자를 soft fail이라고 부른다. soft fail은 가용성에는 유리하지만, 폐기된 인증서를 놓칠 수 있다.

OCSP stapling은 이 경계를 완화한다. 서버가 미리 받은 폐기 상태 응답을 붙여 주면 클라이언트가 CA에 직접 질의하지 않아도 된다. 그래도 stapling이 만료되거나 누락되는 경우를 모니터링해야 한다.

6-7. session resumption은 성능 최적화이지 보안 마법이 아니다

섹션 제목: “6-7. session resumption은 성능 최적화이지 보안 마법이 아니다”

Session resumption은 handshake latency를 줄인다. 하지만 ticket key 관리가 부실하면 ticket으로 재개되는 세션의 보안 속성이 약해질 수 있다. 0-RTT까지 켜면 replay 경계가 추가된다.

처음 학습할 때는 “풀 핸드셰이크를 줄이는 캐시” 정도로 이해하고, 운영에서는 ticket key 회전, PSK 수명, 0-RTT 허용 경로를 함께 본다.


7. 실패 신호: 증상에서 개념으로 되돌아가기

섹션 제목: “7. 실패 신호: 증상에서 개념으로 되돌아가기”

TLS 장애는 메시지가 길고 도구마다 표현이 다르다. 먼저 증상을 개념 축으로 분류하면 원인을 좁히기 쉽다.

증상깨진 축대표 원인첫 관찰
certificate has expired인증서 수명주기갱신 실패, 리스너 교체 실패notAfter, ACM/cert-manager 상태
unable to get local issuer certificate인증서 체인Intermediate 누락, trust store 차이showcerts 체인 길이
hostname mismatchSAN/호스트명 검증잘못된 인증서, SNI 누락, IP 직접 접속URL host와 SAN 비교
handshake failure버전/cipher/mTLSTLS 정책 불일치, client cert 미제출서버 로그, client TLS 버전
mixed content 경고브라우저 하위 리소스HTTP script/image/API URL브라우저 콘솔, CSP report
HSTS로 HTTP 접속 불가브라우저 정책긴 max-age, preload, 하위 도메인 미준비response header, preload 상태
0-RTT 중복 처리replay 경계비멱등 요청 early data 허용Early-Data: 1, idempotency log
CT에서 모르는 인증서 발견발급 감시오발급, DNS/CA 계정 침해 가능성CT monitor, CA audit

이 표는 장애 대응 순서를 대신하지 않는다. 대신 긴 오류 문자열을 “인증서 체인”, “호스트명”, “수명주기”, “브라우저 경계”, “재전송 경계” 같은 학습 개념으로 되돌리는 지도다.

TLS 장애를 볼 때는 다음 순서로 좁히면 중복 조사를 줄일 수 있다.

  1. 연결한 호스트명이 의도한 DNS 이름인가?
  2. 서버가 그 호스트명에 맞는 SAN을 가진 인증서를 보냈는가?
  3. Leaf에서 Root까지 체인이 이어지는가?
  4. 협상된 TLS 버전과 cipher suite가 클라이언트와 서버 정책에 맞는가?
  5. 암호가 풀린 뒤의 프록시·백엔드·브라우저 정책에서 다시 깨지는가?

예를 들어 브라우저는 ERR_CERT_COMMON_NAME_INVALID를 보여주고, curl은 subject name mismatch를 보여줄 수 있다. 표현은 달라도 1-2번 질문의 실패다. 반대로 403은 TLS handshake가 끝난 뒤 애플리케이션이나 mTLS authorization에서 거부한 결과일 수 있으므로, 인증서 검증 실패와 분리해야 한다.

브라우저: ERR_CERT_DATE_INVALID
curl: SSL certificate problem: certificate has expired
openssl: Verify return code: 10 (certificate has expired)

의미는 단순하다. 서버가 제시한 Leaf 인증서의 Not After가 지났다. 원인은 보통 자동 갱신 실패, DNS 검증 레코드 누락, 새 인증서를 리스너에 연결하지 못한 배포 실패다.

학습 포인트는 “인증서 발급 성공”과 “서비스가 계속 유효한 인증서를 제공”이 다르다는 점이다. 인증서 수명주기는 발급, 검증 레코드 유지, 갱신, 리스너 반영, 알림까지 하나의 체인이다.

openssl: Verify return code: 20 (unable to get local issuer certificate)
curl: SSL certificate problem: unable to get local issuer certificate

이 신호는 서버가 Leaf 인증서만 보내고 Intermediate CA를 보내지 않았을 때 자주 발생한다. Let’s Encrypt나 다른 CA에서 받은 cert.pem만 설정하고 fullchain.pem을 빠뜨리면 나타난다.

판단 기준은 다음과 같다.

관찰의미
브라우저는 되는데 curl 실패브라우저 캐시가 체인을 보완했을 수 있음
Android 구버전만 실패trust store와 체인 빌딩 차이 가능
showcerts에 Leaf만 보임서버 체인 제공 누락 가능
curl: SSL: no alternative certificate subject name matches target host name

호스트명 검증이 실패했다는 뜻이다. 흔한 원인은 IP로 직접 접속했거나, SNI 없이 접속해 로드밸런서의 기본 인증서를 받았거나, 인증서 SAN에 실제 API 도메인이 빠진 경우다.

예를 들어 https://54.1.2.3으로 접속하면 클라이언트가 호스트명을 IP로 검증한다. 인증서 SAN은 보통 api.example.com이지 IP가 아니므로 실패한다. 이때는 DNS 이름으로 접속하고 SNI가 전달되는지 확인해야 한다.

HTTP/2 403
x-amzn-mtls-clientcert-validity: FAILED
또는
tls: bad certificate
ssl alert handshake failure

이 신호는 서버가 클라이언트 인증서를 요구했지만, 클라이언트가 인증서를 보내지 않았거나, TrustStore에 없는 CA가 서명한 인증서를 보냈거나, 인증서가 만료되었을 때 나타난다.

mTLS 장애는 “서버 인증서”와 “클라이언트 인증서”를 분리해서 본다.

  1. 클라이언트가 서버 인증서를 신뢰하는가?
  2. 서버가 클라이언트 인증서를 요청했는가?
  3. 클라이언트가 올바른 인증서와 개인키를 보냈는가?
  4. 서버 TrustStore가 그 인증서의 issuer를 신뢰하는가?
  5. 인증서의 SAN 또는 SPIFFE ID가 정책과 맞는가?

Handshake timeout은 TLS 자체의 암호 문제일 수도 있지만, 네트워크 지연, 로드밸런서 보안 정책, 클라이언트 cipher suite 불일치, SNI 누락, 서버 CPU 포화도 원인이 될 수 있다.

구분 기준은 다음과 같다.

증상먼저 의심할 것
특정 구형 클라이언트만 실패TLS 버전 또는 cipher suite 정책
IP 직접 접속만 실패SNI 또는 SAN 불일치
피크 시간에만 증가handshake CPU, connection surge, rate limit
특정 리전에서만 증가네트워크 RTT 또는 edge/origin 경로
인증서 교체 직후 증가체인 누락, SAN 누락, 새 인증서 연결 문제
브라우저 콘솔:
Mixed Content: The page at 'https://app.example.com' was loaded over HTTPS,
but requested an insecure script 'http://cdn.example.com/app.js'.

이 신호는 최상위 문서는 HTTPS지만 하위 리소스가 HTTP라는 뜻이다. 스크립트와 XHR/fetch는 보통 차단되고, 일부 이미지 같은 리소스는 경고 또는 자동 업그레이드 정책의 영향을 받을 수 있다.

해결의 핵심은 “자물쇠를 복구한다”가 아니라 모든 하위 리소스의 URL 생성 경로를 HTTPS 기준으로 바꾸는 것이다. CDN, CMS, HTML 템플릿, API base URL, redirect target을 함께 본다.

짧은 완화책으로 Content-Security-Policy: upgrade-insecure-requests를 둘 수 있지만, 이것은 HTTP URL을 영구히 괜찮게 만드는 면죄부가 아니다. 원본 URL 생성 코드를 HTTPS 기준으로 고치고, 차단되는 active content가 남는지 브라우저 콘솔과 CSP report로 확인해야 한다.

자주 보는 오류 매핑:

오류 메시지먼저 의심할 것
self signed certificate신뢰된 CA 체인이 아닌 자체 서명 인증서
unable to verify the first certificateintermediate CA 누락 또는 체인 순서 문제
hostname mismatchSAN에 접속 호스트명이 없음

HSTS preload 또는 긴 max-age를 켠 뒤 인증서가 만료되면, 사용자는 HTTP fallback으로 우회할 수 없다. 보안상 맞는 동작이지만, 운영 복구에서는 압박이 커진다.

실패 신호는 “브라우저가 무조건 HTTPS로만 접속하고, 사용자가 예외를 추가할 여지도 줄어든다”는 것이다. 그래서 HSTS는 인증서 자동 갱신과 모니터링이 안정된 뒤 강하게 적용한다.

CT 로그 모니터링에서 모르는 인증서가 발견되면, 누군가 도메인에 대한 인증서를 발급받았다는 뜻이다. 항상 침해라는 의미는 아니지만, DNS 검증 권한, CA 계정, 자동화 파이프라인을 확인해야 한다.

CT의 학습 포인트는 “신뢰 체인도 관찰 대상”이라는 점이다. CA가 잘못된 인증서를 발급하는 사건은 드물지만, 발생하면 TLS의 인증 보장이 흔들린다.


이 부록은 본문 이해에 필수는 아니다. 실제 서버에서 TLS 상태를 관찰할 때 필요한 최소 명령만 모았다.

인증서 체인과 주요 필드 보기
Terminal window
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null

먼저 볼 것:

Certificate chain
Verify return code: 0 (ok)
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384

인증서 자체 필드는 다음처럼 확인한다.

Terminal window
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
SNI 차이 관찰
Terminal window
# SNI 없이 IP로 접속하면 기본 인증서가 올 수 있음
openssl s_client -connect 54.1.2.3:443 </dev/null
# SNI를 넣으면 해당 호스트의 인증서를 요청함
openssl s_client -connect 54.1.2.3:443 -servername api.example.com </dev/null

관찰 포인트는 SAN이다. 두 출력의 인증서 주체와 SAN이 다르면, 로드밸런서가 SNI를 보고 인증서를 고른다는 뜻이다.

curl로 TLS handshake 시간 보기
Terminal window
curl -o /dev/null -s -w \
"connect=%{time_connect}s appconnect=%{time_appconnect}s total=%{time_total}s\n" \
https://api.example.com/health

time_appconnect - time_connect가 TCP 연결 이후 TLS 핸드셰이크에 걸린 시간이다. 이 값이 특정 배포 이후 커졌다면 TLS 정책, 인증서 체인, edge/origin 경로를 함께 본다.

mTLS 클라이언트 인증서 제시
Terminal window
curl --cert client.crt --key client.key --cacert ca.crt \
https://internal-api.example.com/health

이 명령은 “클라이언트가 인증서를 제대로 보냈는가”를 보는 최소 관찰용이다. 실패하면 서버 TrustStore, client cert issuer, 만료일, SAN 또는 SPIFFE ID를 순서대로 확인한다.

인증서 만료일만 빠르게 보기
Terminal window
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -dates

출력 예시:

notBefore=Jan 1 00:00:00 2026 GMT
notAfter=Apr 1 00:00:00 2026 GMT

학습 포인트는 날짜 자체보다 이 값을 모니터링과 알림으로 연결하는 것이다.


TLS의 세 보장
기밀성: 중간자가 HTTP 내용을 읽지 못함
무결성: 중간자가 바이트를 바꾸면 검증 실패
인증: 인증서 체인과 SAN으로 서버 신원 검증
키 역할
ECDHE와 인증서: 핸드셰이크에서 신뢰와 세션 키 재료 합의
AEAD 대칭키: 실제 HTTP 요청·응답 암호화와 무결성 검증
핸드셰이크
TLS 1.2: 보통 2 RTT
TLS 1.3: 보통 1 RTT, 전방 보안 중심, 0-RTT는 replay 경계 주의
운영 판단
SNI: 한 IP에서 어떤 인증서를 낼지 결정
ALPN: TLS 안에서 HTTP/2 같은 상위 프로토콜 협상
mTLS: 클라이언트도 인증서로 신원 증명
TLS termination: 어디서 암호가 풀리는지 결정
HSTS/mixed content: 브라우저 경계에서 HTTPS 경험을 완성하거나 깨뜨림

TLS 학습 체크

  • TLS가 제공하는 기밀성, 무결성, 인증을 서로 구분해 설명할 수 있다.
  • AEAD, ECDHE, X.509, CA, SAN이 핸드셰이크에서 맡는 역할을 말할 수 있다.
  • TLS 1.2와 TLS 1.3의 RTT 차이와 TLS 1.3의 key_share 설계를 설명할 수 있다.
  • 0-RTT가 왜 상태 변경 요청에 위험한지 반례로 설명할 수 있다.
  • SNI와 ALPN이 각각 인증서 선택과 상위 프로토콜 협상에 쓰인다는 것을 안다.
  • mTLS가 인증을 강화하지만 권한 부여를 대체하지 않는다는 경계를 안다.
  • ALB, Nginx, CDN, service mesh 중 어디서 TLS를 종료할지 선택 기준을 말할 수 있다.
  • 인증서 만료, chain 누락, SAN 불일치, mixed content, HSTS preload의 실패 신호를 구분할 수 있다.

TLS는 인증서 체인으로 서버 신원을 확인하고, ECDHE로 연결마다 다른 세션 키 재료를 합의한 뒤, AEAD 대칭키로 HTTP 바이트를 빠르게 암호화·검증하는 채널 보안 계층이다. HTTPS 운영의 핵심은 이 메커니즘을 이해한 상태에서 인증서 수명주기, TLS 종료 위치, 브라우저 경계, 실패 신호를 함께 관리하는 것이다.