기밀성
HTTP 요청과 응답을 세션 키로 암호화해 중간 네트워크 장비가 내용을 읽지 못하게 한다.
쿠키, Authorization 헤더, 개인정보, 결제 정보가 네트워크를 지날 때TLS(Transport Layer Security)는 두 엔드포인트 사이의 바이트 스트림에 기밀성, 무결성, 인증을 붙이는 보안 프로토콜이다. HTTPS(HTTP over TLS)는 HTTP 요청과 응답을 TLS가 만든 안전한 채널 위에서 주고받는 조합이다.
중요한 점은 “암호화됨”이 전부가 아니라는 것이다. 안전한 HTTPS 연결은 요청 본문을 숨기고, 중간 변조를 감지하며, 접속한 호스트가 기대한 서버인지 검증해야 한다.
퀴즈
TLS는 HTTP 바이트를 암호화할 뿐 아니라 변조를 감지하고, 인증서 체인을 통해 서버 신원을 검증한다. 세 보장 중 하나가 빠지면 안전한 HTTPS 연결이라고 보기 어렵다.
이 문서는 L1 web-security-basics에서 배운 MITM(Man-in-the-Middle, 중간자 공격) 방어와 L2 tcp-udp-internals.mdx에서 배운 TCP 연결 수립 감각 위에서 시작한다. L1에서는 “HTTPS를 쓰면 도청과 변조를 막는다”까지 다뤘고, 여기서는 그 결론을 가능하게 만드는 핸드셰이크, 인증서 체인, 키 교환, 종료 위치, 운영 실패 신호를 학습한다.
평문 HTTP는 요청과 응답을 그대로 네트워크에 흘려보낸다. 같은 Wi-Fi, 잘못된 프록시, 감염된 라우터, DNS 스푸핑 지점에 있는 공격자는 세 가지 일을 할 수 있다.
사용자 브라우저 --HTTP 평문--> 공격자가 보는 네트워크 구간 --> 서버 | | 읽기 가능: Cookie, Authorization, body | 변조 가능: HTML, redirect, JSON fieldTLS가 붙으면 이 그림은 이렇게 바뀐다.
사용자 브라우저 --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 클라이언트는 접속한 호스트명이 이 목록에 들어 있는지 확인한다.
HTTP 요청과 응답을 세션 키로 암호화해 중간 네트워크 장비가 내용을 읽지 못하게 한다.
쿠키, Authorization 헤더, 개인정보, 결제 정보가 네트워크를 지날 때AEAD 또는 MAC 검증으로 전송 중 바이트가 바뀌었는지 확인하고, 검증 실패 시 데이터를 폐기한다.
응답 JSON, HTML, redirect, API 결과가 중간에서 바뀌면 안 될 때X.509 인증서 체인과 호스트명 검증으로 접속한 서버가 기대한 주체인지 판단한다.
공격자가 가짜 서버나 잘못된 인증서를 제시할 가능성을 차단할 때세 보장은 서로 대체되지 않는다. 암호화만 있고 인증이 없으면 공격자가 가짜 서버와 암호화된 채널을 만들 수 있다. 인증만 있고 무결성이 없으면 중간자가 응답을 바꿔도 탐지할 수 없다. 무결성만 있고 기밀성이 없으면 바이트가 바뀌지는 않지만 내용은 그대로 읽힌다.
같은 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.1Host: api.example.comCookie: session=abc123Authorization: Bearer eyJ...평문 HTTP에서는 같은 네트워크 구간의 관찰자가 session=abc123을 읽을 수 있다. TLS에서는 이 요청이 TLS Record Layer 안에서 암호문으로 바뀌고, 수신자는 AEAD tag를 검증한 뒤에야 HTTP 계층으로 넘긴다. 중간자가 /me를 /admin으로 바꾸거나 쿠키 값을 바꾸면 tag 검증이 실패한다.
이 예시가 중요한 이유는 TLS가 “HTTP를 더 똑똑하게 만드는 기능”이 아니라는 점이다. HTTP는 여전히 메서드, 헤더, 상태 코드로 동작하고, TLS는 그 바이트가 이동하는 채널의 읽기·변조·신원 문제를 해결한다.
초기 웹은 평문 HTTP로도 충분했다. 정적 문서 중심이고, 쿠키와 로그인 세션이 오늘날처럼 광범위하게 쓰이지 않았기 때문이다. 하지만 로그인, 결제, 개인화 API가 늘어나면서 “네트워크가 선하다”는 전제가 깨졌다.
TLS의 lineage는 다음 한 문장으로 요약할 수 있다.
평문 HTTP는 도청·변조·가짜 서버를 구분하지 못한다→ TLS가 암호화·무결성·인증을 한 채널에 묶는다→ HTTPS는 HTTP 의미론을 유지하면서 안전한 전송 계층을 얻는다RFC 2818은 HTTP를 TLS 위에서 실행하는 방식을 정의하면서, 기본 포트가 443이고 서버 인증서의 신원이 접속 호스트명과 일치해야 한다고 설명한다. 그래서 HTTPS는 단순히 http://를 https://로 바꾸는 문법이 아니라, “이 호스트와 안전한 채널을 만들었는가”라는 검증 절차다.
TLS의 설계는 두 요구를 동시에 만족해야 한다.
그래서 TLS는 느리지만 신원과 키 합의에 필요한 비대칭키 연산을 핸드셰이크에 집중시키고, 실제 데이터 전송에는 빠른 대칭키를 쓴다.
대칭키 암호화는 같은 키로 암호화와 복호화를 수행한다. 빠르기 때문에 HTTP 본문, 헤더, 쿠키처럼 양이 많은 데이터를 처리하기에 적합하다. 문제는 통신 전에 그 키를 어떻게 안전하게 공유하느냐다.
비대칭키 암호화와 디지털 서명은 공개키와 개인키를 나누어 쓴다. 공개키는 노출되어도 되지만, 개인키는 서버만 가져야 한다. 이 방식은 대칭키보다 훨씬 비싸지만, 처음 만나는 상대와 키를 합의하거나 “이 공개키가 이 서버의 것”임을 증명하는 데 적합하다.
핸드셰이크 단계 - 인증서로 서버 신원 검증 - ECDHE로 세션 키 재료 합의 - 비대칭키와 디지털 서명이 중심
데이터 전송 단계 - 합의한 세션 키로 HTTP record 암호화 - AES-GCM 또는 ChaCha20-Poly1305 같은 AEAD가 중심ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)는 타원곡선 기반의 임시 키 교환 방식이다. “Ephemeral”은 세션마다 새 임시 키를 만들고 버린다는 뜻이다. 이 성질 덕분에 서버 개인키가 나중에 유출되어도 과거 세션의 대칭키를 곧바로 복원할 수 없다.
비대칭키 연산은 연결을 시작할 때는 감당할 수 있지만, 매 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를 읽을 때는 항상 세 질문으로 돌아오면 된다.
이 세 질문이 분리되면 운영 장애도 더 빨리 좁혀진다. 인증서 만료는 “누구와 연결했는가”의 실패이고, TLS 1.0 허용은 “이 연결만의 안전한 키인가”의 실패이며, ALB 뒤 백엔드 HTTP 구간은 “어디서 암호가 풀리는가”의 설계 결정이다.
서버가 “이 공개키가 내 것”이라고 주장하는 것만으로는 부족하다. 공격자도 자기 공개키를 제시할 수 있기 때문이다. TLS는 인증기관(CA, Certificate Authority)이 서명한 X.509 인증서 체인으로 이 문제를 푼다.
Root CA └── Intermediate CA └── Leaf Certificate └── api.example.comRoot 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에 접속하면 다음을 확인한다.
api.example.com이 있는가?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이 맞지 않으면 “나는 어느 서버와 말하는가”라는 인증 보장이 깨진다.
실제 인증서에서 처음 봐야 할 필드는 많지 않다.
Subject: CN=api.example.comIssuer: CN=Amazon RSA 2048 M02, O=Amazon, C=USValidity: Not Before: Jan 1 00:00:00 2026 GMT Not After : Apr 1 00:00:00 2026 GMTSubject Alternative Name: DNS:api.example.com DNS:*.example.comPublic Key Algorithm: id-ecPublicKeySignature Algorithm: ecdsa-with-SHA256이 출력은 다음 질문으로 읽는다.
| 필드 | 읽는 법 | 실패하면 보이는 현상 |
|---|---|---|
Subject | 인증서 주체의 사람이 읽는 이름 | CN만 보고 판단하면 SAN 누락을 놓칠 수 있음 |
Issuer | 누가 이 인증서를 서명했는가 | 신뢰 체인이 끊기면 issuer 관련 오류 |
Not After | 언제 만료되는가 | 만료 후 certificate has expired |
Subject Alternative Name | 어떤 호스트명에 유효한가 | hostname mismatch 또는 브라우저 경고 |
Public Key Algorithm | 서버 공개키 알고리즘 | 구형 클라이언트 호환성과 성능 판단 |
Signature Algorithm | CA가 어떤 방식으로 인증서를 서명했는가 | 약한 서명 알고리즘이면 정책상 거부 가능 |
여기서 와일드카드 *.example.com은 api.example.com에는 맞지만 v1.api.example.com에는 보통 맞지 않는다. 와일드카드는 인증서 수를 줄이는 도구이지, 모든 하위 도메인을 무한히 덮는 신뢰 모델이 아니다.
다음 두 출력은 둘 다 Leaf 인증서 자체는 정상처럼 보인다. 차이는 서버가 Intermediate CA를 함께 보내는지다.
정상 체인0 s:CN=api.example.com i:CN=R11, O=Let's Encrypt, C=US1 s:CN=R11, O=Let's Encrypt, C=US i:CN=ISRG Root X1, O=Internet Security Research Group, C=USVerify return code: 0 (ok)불완전한 체인0 s:CN=api.example.com i:CN=R11, O=Let's Encrypt, C=USVerify return code: 20 (unable to get local issuer certificate)첫 번째 출력은 Leaf의 issuer인 R11 인증서를 서버가 같이 보냈다. 클라이언트는 R11이 ISRG Root X1로 이어지는지 확인할 수 있다. 두 번째 출력은 Leaf만 있으므로, 클라이언트가 R11을 이미 알고 있지 않다면 체인을 완성할 수 없다.
이 예시는 cert.pem과 fullchain.pem의 차이를 설명한다. cert.pem은 Leaf만 담는 경우가 많고, fullchain.pem은 Leaf와 Intermediate를 함께 담는다. Nginx나 Apache에서 서버 인증서 파일을 고를 때 “내 인증서가 맞는가”뿐 아니라 “클라이언트가 Root까지 올라갈 길을 받는가”를 확인해야 한다.
다음 인증서는 같은 회사 도메인을 담고 있어도 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 사용자 트래픽을 신뢰받는 일이 줄어든다.
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 keyTLS 1.2에서도 ECDHE 기반 cipher suite를 쓰면 전방 보안이 가능하다. 하지만 TLS 1.2는 역사적으로 RSA key exchange, 오래된 CBC cipher suite, 구버전 프로토콜 fallback 같은 위험한 선택지가 함께 남아 있었다. 그래서 운영 정책에서는 TLS 1.0/1.1을 비활성화하고, TLS 1.2 이상만 허용하는 쪽으로 수렴한다.
TLS 1.3은 ClientHello에 key_share를 미리 넣는다. 클라이언트가 “서버가 이 타원곡선 그룹을 지원할 것”이라고 추측하고 ECDHE 공개키를 먼저 보내므로, 서버는 첫 응답에서 바로 세션 키 재료를 계산할 수 있다.
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의 핵심 개선은 세 가지다.
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로 올릴지 결정할 때서울 사용자가 같은 리전의 edge에 접속해 RTT가 20ms라면 TLS 1.2와 TLS 1.3의 차이는 작게 느껴질 수 있다.
TCP handshake: 1 RTT = 20msTLS 1.2 handshake: 2 RTT = 40ms첫 HTTP 요청 전 최소 지연: 약 60ms
TCP handshake: 1 RTT = 20msTLS 1.3 handshake: 1 RTT = 20ms첫 HTTP 요청 전 최소 지연: 약 40ms하지만 사용자가 먼 리전의 origin에 직접 붙어 RTT가 160ms라면 차이가 커진다.
TLS 1.2: TCP 160ms + TLS 320ms = HTTP 전 약 480msTLS 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 = x25519Server: HelloRetryRequest, please use secp256r1ClientHello: key_share = secp256r1ServerHello ...운영에서 이 반례가 자주 보이면 클라이언트와 서버의 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 184233TLSv1.2 1287TLSv1.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.1Idempotency-Key: 없음이 요청이 0-RTT early data로 전송되고 재생되면 중복 결제가 발생할 수 있다. 반대로 정적 파일 GET은 부작용이 없고 캐시 가능한 경우가 많아 0-RTT 후보가 될 수 있다.
| 요청 유형 | 0-RTT 판단 | 이유 |
|---|---|---|
GET /assets/app.js | 조건부 허용 가능 | 부작용이 없고 재생되어도 상태가 바뀌지 않음 |
HEAD /health | 허용 가능 | 관찰용 요청이며 상태 변경이 없음 |
POST /payments | 금지 | 재생되면 중복 결제 가능 |
PATCH /profile | 금지 | 사용자 상태가 중복 변경될 수 있음 |
POST /search | API 의미를 따로 확인 | POST라도 실제 부작용이 없을 수 있으나 명시 필요 |
0-RTT 판단은 HTTP 메서드 이름만으로 끝나지 않는다. origin 서버가 그 경로의 부작용을 알고 있어야 하고, 게이트웨이는 위험한 early data를 425 Too Early로 재시도시키는 정책을 가질 수 있다.
퀴즈
힌트: 빠른 전송이 항상 안전한 전송은 아닙니다.
0-RTT early data는 재전송될 수 있으므로 상태 변경 요청이 중복 실행될 위험이 있다. 성능 이득보다 중복 결제 같은 정합성 손상이 더 크다.
SNI(Server Name Indication)는 TLS ClientHello에 목적지 호스트명을 넣는 확장이다. 하나의 IP 주소나 로드밸런서가 여러 도메인을 처리할 때, 서버는 SNI를 보고 어떤 인증서를 제시할지 결정한다.
ClientHello server_name: api.example.com supported_versions: TLS 1.3 key_share: x25519 public keySNI가 없으면 서버는 기본 인증서를 내보낼 수밖에 없다. 그래서 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: h2SNI와 ALPN은 모두 “TLS가 HTTP를 모른다”는 직관을 조금 수정하게 만든다. TLS는 HTTP 본문 의미를 해석하지 않지만, 어떤 인증서를 쓸지와 어떤 상위 프로토콜로 넘길지는 핸드셰이크에서 협상한다.
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 정책이나 서비스 메시 정책이 이어서 판단한다.
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 내부를 신뢰할 수 있는지, 규정상 종단간 암호화가 필요한지, 서비스 간 신원 확인이 필요한지를 따로 판단해야 한다.
사용자가 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 설정 |
인증서는 언젠가 만료된다. 그래서 TLS 운영에서 더 중요한 질문은 “어떻게 발급하는가”보다 “누가 언제 갱신 실패를 알아차리는가”다.
AWS ACM(AWS Certificate Manager)은 AWS 로드밸런서, CloudFront 같은 서비스에 붙는 공개 인증서의 발급과 갱신을 관리한다. DNS 검증을 유지하면 자동 갱신 흐름을 기대할 수 있지만, 검증용 CNAME을 지우거나 인증서가 AWS 서비스에 연결되어 있지 않으면 갱신이 실패할 수 있다.
Let’s Encrypt는 공개 CA가 무료로 짧은 수명의 인증서를 발급하는 생태계다. 보통 certbot이나 cert-manager 같은 자동화 도구와 함께 쓴다. 유효기간이 짧기 때문에 수동 갱신을 전제로 두면 장애가 된다.
| 환경 | 기본 선택지 | 확인해야 할 실패 신호 |
|---|---|---|
| ALB/CloudFront | ACM 공개 인증서 | DNS 검증 레코드 누락, 만료 알림 미수신 |
| EC2/Nginx 직접 노출 | Let’s Encrypt + 자동 갱신 | cron/systemd timer 실패, fullchain.pem 누락 |
| Kubernetes Ingress | cert-manager + ACME issuer | Secret 갱신 실패, issuer challenge 실패 |
| 내부 서비스 간 mTLS | Private CA, SPIFFE/SPIRE, mesh CA | trust bundle 불일치, 짧은 SVID 회전 실패 |
| 규제 환경의 종단간 TLS | Private CA 또는 관리형 CA | 백엔드 인증서 만료, 체인 불완전 |
ACM 문서 기준으로 DNS 검증 인증서는 만료 전에 갱신 조건을 점검하고 실패 이벤트를 AWS Health/EventBridge로 전달한다. 운영 판단의 핵심은 “30일 전 알림 하나”보다, 검증 레코드와 연결 상태가 깨지는 시점에 신호를 받는 것이다.
Let’s Encrypt 계열의 짧은 수명 인증서를 90일짜리로 받았다고 하자. 안전한 자동화는 대략 다음 사건을 만든다.
Day 0 인증서 발급, Nginx reload 성공Day 55 자동 갱신 시도 시작Day 60 갱신 성공, 새 fullchain 배치, reload 성공Day 61 외부 probe가 새 Not After 확인Day 75 아직 갱신 안 됐으면 warningDay 83 아직 갱신 안 됐으면 criticalDay 90 만료, 사용자 장애여기서 중요한 관찰은 certbot renew --dry-run 성공과 실제 서비스 성공이 다르다는 점이다. 갱신 명령이 성공해도 Nginx reload가 실패하면 프로세스는 예전 인증서를 계속 제공할 수 있다. 따라서 외부에서 notAfter를 읽는 probe가 필요하다.
ACM도 같은 원리다. 발급은 AWS가 관리하더라도 DNS 검증 CNAME이 사라지면 자동 갱신 조건이 깨질 수 있다. “관리형 인증서”는 “아무 것도 관찰하지 않아도 되는 인증서”가 아니다.
Let’s Encrypt 계열 파일명은 처음 보면 헷갈린다.
| 파일 | 보통 담긴 내용 | 서버 설정에서의 의미 |
|---|---|---|
cert.pem | Leaf 인증서 | 단독 설정하면 chain 누락 위험 |
chain.pem | Intermediate 인증서 | Leaf와 함께 제공되어야 함 |
fullchain.pem | Leaf + Intermediate | Nginx ssl_certificate에 주로 사용 |
privkey.pem | 개인키 | 외부 노출 금지, 권한 관리 필요 |
학습할 때는 fullchain.pem과 privkey.pem의 짝을 기억하면 된다. 공개로 보내도 되는 것은 인증서 체인이고, 절대 노출되면 안 되는 것은 개인키다. 개인키가 유출되면 새 인증서를 발급하는 것만으로 끝나지 않고, 기존 인증서 폐기와 배포 경로 점검이 필요하다.
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 보조 장치는 서로 겹쳐 보이지만, 감시하는 실패 면이 다르다.
브라우저는 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; preloadHSTS는 강력하지만 되돌리기 어렵다. includeSubDomains와 preload를 적용하면 아직 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,000TLS 1.2: 7,950TLS 1.0: 50TLS 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 edge | ACM + DNS 검증 + EventBridge 알림 | 검증 CNAME을 임시 레코드처럼 취급 |
| VM 또는 bare metal Nginx | Let’s Encrypt 자동 갱신 + reload 검증 | cert.pem만 설정하고 chain 누락 |
| Kubernetes Ingress | cert-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 없는 POSTidempotency key가 있어도 0-RTT가 자동으로 안전해지는 것은 아니다. 서버가 중복 요청을 실제로 같은 결과로 접어야 하고, 여러 인스턴스 사이에서 같은 key를 일관되게 볼 수 있어야 한다.
실패 신호는 “같은 idempotency key가 서로 다른 인스턴스에서 각각 새 요청처럼 처리됐다”는 로그다. 0-RTT anti-replay 상태가 노드 로컬 메모리에만 있거나, edge와 origin의 정책이 다르면 재전송을 한 번만 처리한다는 보장이 깨진다.
조건은 다음과 같다.
사용자 -> app.example.comAPI -> api.example.com인프라 -> Route 53, ALB, ECS규제 -> 일반 개인정보, 강한 종단간 암호화 요구 없음팀 역량 -> 인증서 수동 운영 경험 적음이 경우 기본 선택은 ALB에서 TLS를 종료하고 ACM 인증서를 붙이는 것이다. 이유는 인증서 갱신 부담을 줄이고, ALB access log와 WAF에서 HTTP 정보를 볼 수 있으며, ECS task는 HTTP 서버로 단순하게 운영할 수 있기 때문이다.
하지만 이 결정에도 체크포인트가 있다.
api.example.com과 app.example.com이 인증서 SAN에 포함되는가?X-Forwarded-Proto를 신뢰하도록 proxy 설정이 되어 있는가?조건은 다음과 같다.
order-service -> payment-service환경 -> Kubernetes요구 -> 서비스 신원 확인, 내부자/오배포 방어트래픽 -> 내부 east-west traffic이 경우 mTLS 또는 서비스 메시를 검토할 수 있다. 목표는 “네트워크가 내부니까 믿는다”를 줄이고, 호출 주체가 회사가 발급한 workload identity를 가진 서비스인지 확인하는 것이다.
도입 전에 물어볼 질문은 다음과 같다.
조건은 다음과 같다.
사용자 -> 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 확인-> 만료 임계값 알림이 정상 상태로 돌아옴이 시나리오에서 긴 명령을 외우는 것보다 중요한 것은 체인의 각 단계가 독립 실패할 수 있다는 점이다.
HTTPS는 전송 중 도청과 변조를 막는다. 서버가 받은 뒤의 SQL Injection, XSS, 권한 체크 누락, 안전하지 않은 파일 업로드는 TLS가 해결하지 않는다.
HTTPS가 막는 것 - 네트워크 중간자의 쿠키 도청 - 응답 바이트 변조 - 가짜 서버 인증서
HTTPS가 막지 못하는 것 - 서버가 의도적으로 내려준 취약한 JavaScript - DB에 저장된 평문 비밀번호 - 인증된 사용자의 권한 상승 버그이 경계는 L1의 Defense in Depth와 이어진다. 채널이 안전해야 하지만, 채널만 안전하다고 앱이 안전해지는 것은 아니다.
브라우저의 보안 표시가 정상이어도 애플리케이션 내부의 API 권한 버그는 남을 수 있다. 반대로 서버 인증서는 정상이어도 mixed content 하나 때문에 페이지 보안 표시가 깨질 수 있다.
자물쇠 신호를 읽을 때는 세 가지를 분리한다.
SNI는 ClientHello에 들어가므로 전통적인 TLS에서는 관찰 가능한 메타데이터다. 즉 중간자는 HTTP path나 cookie는 읽지 못해도, 사용자가 어느 호스트명에 접속하려는지는 볼 수 있다.
이 경계는 CDN, 사내 프록시, 감시 회피 같은 주제에서 중요해진다. 다만 이 문서의 핵심은 SNI privacy가 아니라 “서버가 어떤 인증서를 선택하는가”다. 더 깊은 주제는 ECH(Encrypted ClientHello) 같은 별도 확장으로 넘어간다.
Nginx에 cert.pem만 설정하고 fullchain.pem을 쓰지 않으면 어떤 최신 브라우저에서는 중간 인증서를 캐시로 보완해 통과할 수 있다. 그러나 구형 Android, 일부 IoT, curl, Java 클라이언트에서는 unable to get local issuer certificate로 실패할 수 있다.
이 반례는 “내 브라우저에서 열리니 정상”이라는 판단을 깨뜨린다. TLS는 다양한 클라이언트 trust store와 체인 빌딩 정책 위에서 검증된다.
HSTS preload에 등록하면 주요 브라우저가 해당 도메인을 처음부터 HTTPS로만 접속한다. SSL strip 공격에는 강하지만, 잘못 등록하면 HTTPS 준비가 안 된 서브도메인도 HTTP fallback을 할 수 없다.
처음에는 다음 순서가 안전하다.
max-age=300 같은 짧은 값으로 배포한다.includeSubDomains를 적용한다.브라우저와 클라이언트는 OCSP 서버가 응답하지 않을 때 연결을 막을 수도 있고 통과시킬 수도 있다. 후자를 soft fail이라고 부른다. soft fail은 가용성에는 유리하지만, 폐기된 인증서를 놓칠 수 있다.
OCSP stapling은 이 경계를 완화한다. 서버가 미리 받은 폐기 상태 응답을 붙여 주면 클라이언트가 CA에 직접 질의하지 않아도 된다. 그래도 stapling이 만료되거나 누락되는 경우를 모니터링해야 한다.
Session resumption은 handshake latency를 줄인다. 하지만 ticket key 관리가 부실하면 ticket으로 재개되는 세션의 보안 속성이 약해질 수 있다. 0-RTT까지 켜면 replay 경계가 추가된다.
처음 학습할 때는 “풀 핸드셰이크를 줄이는 캐시” 정도로 이해하고, 운영에서는 ticket key 회전, PSK 수명, 0-RTT 허용 경로를 함께 본다.
TLS 장애는 메시지가 길고 도구마다 표현이 다르다. 먼저 증상을 개념 축으로 분류하면 원인을 좁히기 쉽다.
| 증상 | 깨진 축 | 대표 원인 | 첫 관찰 |
|---|---|---|---|
certificate has expired | 인증서 수명주기 | 갱신 실패, 리스너 교체 실패 | notAfter, ACM/cert-manager 상태 |
unable to get local issuer certificate | 인증서 체인 | Intermediate 누락, trust store 차이 | showcerts 체인 길이 |
hostname mismatch | SAN/호스트명 검증 | 잘못된 인증서, SNI 누락, IP 직접 접속 | URL host와 SAN 비교 |
handshake failure | 버전/cipher/mTLS | TLS 정책 불일치, 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 장애를 볼 때는 다음 순서로 좁히면 중복 조사를 줄일 수 있다.
예를 들어 브라우저는 ERR_CERT_COMMON_NAME_INVALID를 보여주고, curl은 subject name mismatch를 보여줄 수 있다. 표현은 달라도 1-2번 질문의 실패다. 반대로 403은 TLS handshake가 끝난 뒤 애플리케이션이나 mTLS authorization에서 거부한 결과일 수 있으므로, 인증서 검증 실패와 분리해야 한다.
브라우저: ERR_CERT_DATE_INVALIDcurl: SSL certificate problem: certificate has expiredopenssl: 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 403x-amzn-mtls-clientcert-validity: FAILED
또는tls: bad certificatessl alert handshake failure이 신호는 서버가 클라이언트 인증서를 요구했지만, 클라이언트가 인증서를 보내지 않았거나, TrustStore에 없는 CA가 서명한 인증서를 보냈거나, 인증서가 만료되었을 때 나타난다.
mTLS 장애는 “서버 인증서”와 “클라이언트 인증서”를 분리해서 본다.
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 certificate | intermediate CA 누락 또는 체인 순서 문제 |
hostname mismatch | SAN에 접속 호스트명이 없음 |
HSTS preload 또는 긴 max-age를 켠 뒤 인증서가 만료되면, 사용자는 HTTP fallback으로 우회할 수 없다. 보안상 맞는 동작이지만, 운영 복구에서는 압박이 커진다.
실패 신호는 “브라우저가 무조건 HTTPS로만 접속하고, 사용자가 예외를 추가할 여지도 줄어든다”는 것이다. 그래서 HSTS는 인증서 자동 갱신과 모니터링이 안정된 뒤 강하게 적용한다.
CT 로그 모니터링에서 모르는 인증서가 발견되면, 누군가 도메인에 대한 인증서를 발급받았다는 뜻이다. 항상 침해라는 의미는 아니지만, DNS 검증 권한, CA 계정, 자동화 파이프라인을 확인해야 한다.
CT의 학습 포인트는 “신뢰 체인도 관찰 대상”이라는 점이다. CA가 잘못된 인증서를 발급하는 사건은 드물지만, 발생하면 TLS의 인증 보장이 흔들린다.
이 부록은 본문 이해에 필수는 아니다. 실제 서버에서 TLS 상태를 관찰할 때 필요한 최소 명령만 모았다.
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null먼저 볼 것:
Certificate chainVerify return code: 0 (ok)Protocol : TLSv1.3Cipher : TLS_AES_256_GCM_SHA384인증서 자체 필드는 다음처럼 확인한다.
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 없이 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 -o /dev/null -s -w \ "connect=%{time_connect}s appconnect=%{time_appconnect}s total=%{time_total}s\n" \ https://api.example.com/healthtime_appconnect - time_connect가 TCP 연결 이후 TLS 핸드셰이크에 걸린 시간이다. 이 값이 특정 배포 이후 커졌다면 TLS 정책, 인증서 체인, edge/origin 경로를 함께 본다.
curl --cert client.crt --key client.key --cacert ca.crt \ https://internal-api.example.com/health이 명령은 “클라이언트가 인증서를 제대로 보냈는가”를 보는 최소 관찰용이다. 실패하면 서버 TrustStore, client cert issuer, 만료일, SAN 또는 SPIFFE ID를 순서대로 확인한다.
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 GMTnotAfter=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 경험을 완성하거나 깨뜨림425 Too Early와 replay 경계.TLS는 인증서 체인으로 서버 신원을 확인하고, ECDHE로 연결마다 다른 세션 키 재료를 합의한 뒤, AEAD 대칭키로 HTTP 바이트를 빠르게 암호화·검증하는 채널 보안 계층이다. HTTPS 운영의 핵심은 이 메커니즘을 이해한 상태에서 인증서 수명주기, TLS 종료 위치, 브라우저 경계, 실패 신호를 함께 관리하는 것이다.