콘텐츠로 이동

TLS & HTTPS

TLS & HTTPS: 브라우저 자물쇠 뒤에서 일어나는 일

섹션 제목: “TLS & HTTPS: 브라우저 자물쇠 뒤에서 일어나는 일”

TLS(Transport Layer Security)는 두 엔드포인트 사이의 통신을 암호화·무결성 보장·상호 인증하는 프로토콜이고, HTTPS는 HTTP를 TLS 위에서 실행하는 조합이다.


  • 플랫폼 엔지니어의 기본 책임: ALB, Nginx, API Gateway — 어느 레이어에서 TLS를 종료할지 결정하고, 인증서를 자동 갱신하는 파이프라인을 구축하는 것은 플랫폼 팀의 핵심 업무다.
  • 장애 원인 1위: SSL handshake timeout, certificate expired, UNABLE_TO_VERIFY_LEAF_SIGNATURE — 프로덕션 장애의 상당수가 TLS 설정 문제에서 시작된다.
  • 서비스 메시의 기반: Kubernetes 환경의 서비스 간 mTLS, Istio/Envoy 사이드카 패턴은 모두 TLS를 이해해야 설계할 수 있다.
  • 프론트 → 플랫폼 브릿지: 브라우저 자물쇠 아이콘은 “서버가 신뢰할 수 있다”는 신호다. 이 신호가 왜 뜨는지, 안 뜰 때 무엇을 봐야 하는지 알아야 한다.

L1과의 관계: L1 web-security-basics에서는 HTTPS가 “도청 방지”를 제공한다는 수준에서 언급했다. 이 문서는 그 내부 메커니즘 — 핸드셰이크, 인증서 체인, 키 교환 알고리즘 — 을 다룬다.


3-1. TLS가 해결하는 세 가지 문제

섹션 제목: “3-1. TLS가 해결하는 세 가지 문제”

국제 특급 우편을 생각해보자.

  • 암호화(Confidentiality): 편지 내용을 잠금 상자에 넣어 발신자와 수신자만 열 수 있다. 중간 배송업체는 상자를 볼 수 없다.
  • 무결성(Integrity): 편지 봉투에 납인(seal)을 붙인다. 도착했을 때 납인이 깨져 있으면 중간에 누군가 열어봤다는 증거다.
  • 인증(Authentication): 수신자가 “이 편지가 정말 내가 아는 회사에서 온 것인가?”를 회사 공식 레터헤드와 공증 도장으로 확인한다.
TLS가 제공하는 보장
─────────────────────────────────────────────────────────
문제 해결 수단 알고리즘 예시
─────────────────────────────────────────────────────────
도청 대칭키 암호화 AES-256-GCM
변조 MAC / AEAD HMAC-SHA256
위장(피싱) 인증서 + PKI X.509, RSA, ECDSA
─────────────────────────────────────────────────────────

3-2. 대칭키 vs 비대칭키 — 역할의 분리

섹션 제목: “3-2. 대칭키 vs 비대칭키 — 역할의 분리”

TLS는 두 종류의 암호화를 목적에 따라 다르게 사용한다.

  • 비대칭키: 자물쇠와 열쇠 세트다. 공개 자물쇠(Public Key)는 누구나 잠글 수 있지만, 개인 열쇠(Private Key)가 있어야만 열 수 있다. 느리지만 키를 사전에 공유하지 않아도 된다.
  • 대칭키: 복사된 방 열쇠다. 같은 키로 잠그고 연다. 빠르지만 처음에 어떻게 안전하게 열쇠를 전달하느냐가 문제다.
핸드셰이크 단계 → 비대칭키(RSA / ECDH) 사용
목적: 세션 키(대칭키)를 안전하게 교환
데이터 전송 단계 → 대칭키(AES-256-GCM) 사용
목적: 실제 HTTP 요청/응답 암호화 (속도 중요)

비대칭키 연산은 CPU 비용이 대칭키 대비 1000배 이상 높다. 그래서 핸드셰이크에서만 비대칭키를 쓰고, 이후 통신은 협상된 대칭키로 처리한다.


공증 시스템과 동일하다. 법무부(Root CA)가 공증사무소(Intermediate CA)를 인가하고, 공증사무소가 개인 계약서(Leaf Certificate)에 도장을 찍는다. 법무부 도장은 운영체제나 브라우저에 내장되어 있어 누구나 신뢰한다.

Root CA (최상위 인증기관)
└── Intermediate CA (중간 인증기관)
└── Leaf Certificate (서버 인증서)
└── example.com
신뢰 흐름: 브라우저 → Leaf → Intermediate → Root CA (브라우저 내장)

왜 Intermediate CA가 필요한가?

Root CA의 개인키가 유출되면 인터넷 전체의 신뢰 체계가 무너진다. 그래서 Root CA는 오프라인 HSM(Hardware Security Module)에 보관하고, 온라인에는 Intermediate CA만 노출한다. Intermediate CA가 침해되면 그것만 폐기(revoke)하면 된다.

브라우저가 https://api.example.com에 접속할 때:

1. 서버가 Leaf 인증서 + Intermediate CA 인증서를 전송
2. 브라우저: Leaf 인증서의 서명을 Intermediate CA 공개키로 검증
3. 브라우저: Intermediate CA 인증서의 서명을 Root CA 공개키로 검증
4. 브라우저: Root CA가 내장된 신뢰 저장소(trust store)에 있는지 확인
5. 브라우저: 인증서의 도메인(CN/SAN)이 접속 도메인과 일치하는지 확인
6. 브라우저: 인증서 유효기간 확인
7. 브라우저: CRL(Certificate Revocation List) 또는 OCSP로 폐기 여부 확인
Terminal window
# 실제 서버 인증서 정보 확인
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -text

주요 출력 필드:

Subject: CN=api.example.com
Issuer: CN=Amazon RSA 2048 M02, O=Amazon, C=US
Validity:
Not Before: Jan 1 00:00:00 2025 GMT
Not After : Feb 1 00:00:00 2026 GMT
Subject Alternative Name:
DNS:api.example.com
DNS:*.example.com
Public Key Algorithm: rsaEncryption (2048 bit)
Signature Algorithm: sha256WithRSAEncryption

SAN(Subject Alternative Name): 와일드카드(*.example.com) 또는 여러 도메인을 하나의 인증서에 포함할 수 있다. 현대 브라우저는 CN 대신 SAN만 검증한다.

인증서 체인을 파일로 분리하여 검증:

Terminal window
# 체인에서 각 인증서 추출 (순서: 0=Leaf, 1=Intermediate)
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' > chain.pem
# 각 인증서의 Subject와 Issuer 확인
openssl crl2pkcs7 -nocrl -certfile chain.pem | openssl pkcs7 -print_certs -noout

예상 출력:

subject=/CN=api.example.com
issuer=/CN=Amazon RSA 2048 M02, O=Amazon, C=US
subject=/CN=Amazon RSA 2048 M02, O=Amazon, C=US
issuer=/CN=Amazon Root CA 1, O=Amazon, C=US

각 인증서의 issuer가 다음 인증서의 subject와 일치해야 체인이 완전함.

📖 더 보기: Certificate Chains Explained — Mister PKI — Root/Intermediate/Leaf의 역할과 브라우저 검증 과정. 이 섹션의 인증서 체인 구조와 직접 연결됨


TLS 1.2는 핸드셰이크에 **2 RTT(Round Trip Time)**가 필요하다. 즉, 실제 HTTP 요청을 보내기 전에 왕복이 2번 발생한다.

클라이언트 서버
| |
|──── ClientHello ─────────────────────→ |
| (지원 암호 suite 목록, random_C) |
| |
| ←── ServerHello ────────────────────── |
| (선택된 암호 suite, random_S) |
| ←── Certificate ───────────────────── |
| (서버 인증서 체인) |
| ←── ServerKeyExchange ─────────────── |
| (DH 파라미터, 서버 서명) |
| ←── ServerHelloDone ───────────────── |
| |
|──── ClientKeyExchange ──────────────→ |
| (Premaster Secret, 공개키로 암호화) |
|──── ChangeCipherSpec ───────────────→ |
|──── Finished ───────────────────────→ |
| (핸드셰이크 검증 MAC) |
| |
| ←── ChangeCipherSpec ──────────────── |
| ←── Finished ─────────────────────── |
| |
|════ 암호화된 HTTP 통신 시작 ════════════ |
총 2 RTT 소요 (TCP 3-way handshake 제외)

세션 키 생성 과정 (TLS 1.2):

Premaster Secret (클라이언트가 생성, 서버 공개키로 암호화)
+
random_C (ClientHello의 난수)
+
random_S (ServerHello의 난수)
Master Secret (PRF 함수 적용)
세션 키들 파생:
- client_write_key (클라이언트→서버 암호화)
- server_write_key (서버→클라이언트 암호화)
- client_write_MAC (클라이언트→서버 무결성)
- server_write_MAC (서버→클라이언트 무결성)

3-5. TLS 1.3 핸드셰이크 — 더 빠르고 안전하게

섹션 제목: “3-5. TLS 1.3 핸드셰이크 — 더 빠르고 안전하게”

TLS 1.3은 2018년 RFC 8446으로 표준화되었다. 핵심 개선점은 1 RTT (0-RTT도 가능)와 Forward Secrecy 강제화다.

클라이언트 서버
| |
|──── ClientHello ─────────────────────→ |
| (지원 암호 suite, key_share, |
| supported_versions: TLS 1.3) |
| |
| ←── ServerHello ────────────────────── |
| (key_share 응답) |
| ←── EncryptedExtensions ────────────── | ← 이미 암호화 시작!
| ←── Certificate ───────────────────── |
| ←── CertificateVerify ─────────────── |
| ←── Finished ─────────────────────── |
| |
|──── Finished ───────────────────────→ |
|════ 암호화된 HTTP 통신 시작 ════════════ |
총 1 RTT 소요

TLS 1.2 vs 1.3 비교:

항목 TLS 1.2 TLS 1.3
────────────────────────────────────────────────────────
핸드셰이크 RTT 2 RTT 1 RTT (0-RTT 옵션)
키 교환 RSA or DH ECDHE 전용 (강제)
Forward Secrecy 선택적 항상 보장
암호화 시작 Finished 이후 ServerHello 이후
취약 암호 suite RC4, 3DES 등 허용 제거됨
세션 재개 Session ID / Ticket PSK (Pre-Shared Key)
────────────────────────────────────────────────────────

Forward Secrecy (전방 보안)란?

TLS 1.2 RSA 방식의 문제:
과거 트래픽을 녹화해두면 → 나중에 서버 개인키가 유출되면
→ 모든 과거 트래픽을 복호화 가능
TLS 1.3 ECDHE의 해결:
매 세션마다 임시 키 쌍(ephemeral key)을 생성
→ 세션 종료 후 임시 키 폐기
→ 서버 개인키가 유출되어도 과거 세션은 복호화 불가

0-RTT (Early Data):

재연결 시 클라이언트가 ClientHello와 함께 HTTP 요청 데이터를 바로 전송하는 모드. 성능은 극대화되지만 Replay Attack 위험이 있어 GET 요청 등 멱등성이 보장된 경우에만 사용한다.

TLS 1.3이 1 RTT인 이유 — 핵심 원리:

TLS 1.2에서는 서버가 지원하는 키 교환 방식을 협상한 후에야 클라이언트가 키 교환 파라미터를 보낼 수 있었다 (2 RTT 필요). TLS 1.3은 다르게 동작한다.

TLS 1.3의 핵심 설계: "미리 추측하고 보낸다 (speculative send)"
1. ClientHello에 key_share 포함:
클라이언트가 "서버가 아마 X25519 또는 P-256을 쓸 것"이라 가정하고
해당 공개키를 미리 생성하여 ClientHello에 동봉
→ 서버는 바로 같은 키로 ECDHE 계산 가능
2. ServerHello와 동시에 암호화 시작:
서버가 key_share를 받는 즉시 Master Secret 계산 가능
→ ServerHello 이후 모든 메시지(Certificate, Finished)가 암호화됨
→ 1 RTT 달성
3. 만약 추측이 틀리면 (HelloRetryRequest):
서버가 다른 키 교환 방식 요청 → 2 RTT로 폴백 (드문 경우)

실제 openssl로 TLS 1.3 핸드셰이크 확인:

Terminal window
openssl s_client -connect cloudflare.com:443 -msg 2>/dev/null | head -30

예상 출력:

>>> TLS 1.3, Handshake [length 0512], ClientHello
<<< TLS 1.3, Handshake [length 0122], ServerHello
<<< TLS 1.3, ChangeCipherSpec [length 0001]
<<< TLS 1.3, Handshake [length 0017], EncryptedExtensions ← 이미 암호화
<<< TLS 1.3, Handshake [length 0956], Certificate
<<< TLS 1.3, Handshake [length 0108], CertificateVerify
<<< TLS 1.3, Handshake [length 0036], Finished
---
SSL handshake has read 2847 bytes and written 385 bytes
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384

📖 더 보기: The Illustrated TLS 1.3 Connection — 위 핸드셰이크 메시지의 모든 바이트를 16진수로 분해하여 각 필드의 의미를 설명. 이 섹션의 핸드셰이크 흐름을 byte-level에서 확인할 수 있음


하나의 IP 주소에 여러 도메인을 호스팅하는 경우(공유 호스팅, ALB 등), 서버는 핸드셰이크 시작 시 클라이언트가 어느 도메인에 접속하려는지 알아야 적합한 인증서를 선택할 수 있다. 그런데 IP만으로는 알 수 없다.

SNI는 TLS 핸드셰이크의 ClientHello에 목적지 호스트명을 포함한다.

ClientHello:
server_name: "api.example.com" ← SNI 필드
supported_versions: TLS 1.3
cipher_suites: [TLS_AES_256_GCM_SHA384, ...]
key_share: [x25519 public key]

서버는 SNI를 보고 api.example.com 인증서를 선택하여 응답한다.

실무 영향:

Terminal window
# SNI 없이 접속 시 (IP 직접 접속)
openssl s_client -connect 54.123.45.67:443
# SNI 명시적 지정
openssl s_client -connect 54.123.45.67:443 -servername api.example.com

ALB에서 여러 Target Group을 도메인별로 라우팅할 때, SNI 기반으로 인증서를 선택하고 리스너 규칙을 적용한다.


3-7. mTLS (Mutual TLS) — 서비스 간 인증

섹션 제목: “3-7. mTLS (Mutual TLS) — 서비스 간 인증”
기본 TLS:
클라이언트 → 서버 인증서 검증 ✓
서버 → 클라이언트 인증 ✗ (클라이언트는 익명)
mTLS (Mutual TLS):
클라이언트 → 서버 인증서 검증 ✓
서버 → 클라이언트 인증서 검증 ✓ (양방향 인증)
클라이언트 서버
| |
|──── ClientHello ─────────────────────→ |
| ←── ServerHello + Certificate ──────── |
| ←── CertificateRequest ────────────── | ← 클라이언트 인증서 요청
| |
|──── Certificate (클라이언트 인증서) ──→ |
|──── CertificateVerify (서명) ────────→ |
|──── Finished ───────────────────────→ |
| ←── Finished ─────────────────────── |
|══════════════════════════════════════ |

서비스 메시 (Kubernetes / Istio):

# Istio PeerAuthentication: 네임스페이스 내 mTLS 강제
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # PERMISSIVE(혼용), STRICT(강제), DISABLE

AWS ALB에서 mTLS 설정:

Terminal window
# ALB 리스너에 mTLS 설정 (AWS CLI)
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:... \
--mutual-authentication \
Mode=verify,TrustStoreArn=arn:aws:elasticloadbalancing:...,\
IgnoreClientCertificateExpiry=false

mTLS 인증서 발급 (내부 CA):

Terminal window
# 내부 Root CA 생성
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=Internal CA/O=MyCompany"
# 클라이언트 인증서 발급
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
-subj "/CN=payment-service/O=MyCompany"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out client.crt -days 365

API Gateway → Lambda mTLS:

내부 서비스 간 통신에서 JWT 토큰 방식 대신 mTLS를 사용하면 네트워크 레이어에서 인증이 완성되므로 애플리케이션 코드에서 인증 로직을 제거할 수 있다.


3-8. TLS Termination — 어디서 TLS를 끊을 것인가

섹션 제목: “3-8. TLS Termination — 어디서 TLS를 끊을 것인가”

TLS Termination이란 암호화된 HTTPS 트래픽을 복호화하는 지점을 의미한다.

클라이언트 ──HTTPS──→ [종료 지점] ──HTTP──→ 백엔드 서버
(TLS Termination)

옵션 1: ALB에서 종료 (가장 일반적)

Internet ──HTTPS──→ ALB ──HTTP──→ ECS Task / EC2
(TLS 종료)
장점:
- 인증서 관리를 AWS ACM이 자동화 (갱신 불필요)
- 백엔드는 HTTP만 처리 → 성능 부담 없음
- ALB WAF 연동, 액세스 로그에서 HTTP 헤더 분석 가능
단점:
- ALB → 백엔드 구간은 암호화되지 않음
- 규정 준수(PCI-DSS 등)에서 종단간 암호화 요구 시 부적합

옵션 2: 종단간 암호화 (End-to-End TLS)

Internet ──HTTPS──→ ALB ──HTTPS──→ ECS Task
(패스스루 or 재암호화)
장점:
- 백엔드까지 암호화 유지
- PCI-DSS, HIPAA 규정 충족
단점:
- 백엔드도 인증서 필요 (관리 복잡)
- ALB에서 HTTP 헤더 기반 라우팅 불가 (패스스루 시)

옵션 3: Nginx에서 종료

nginx.conf
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# TLS 1.2 이상만 허용
ssl_protocols TLSv1.2 TLSv1.3;
# 강력한 암호 suite만 허용
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
# HSTS (브라우저에 HTTPS 강제 지시)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

3-9. AWS ACM & Let’s Encrypt — 인증서 관리

섹션 제목: “3-9. AWS ACM & Let’s Encrypt — 인증서 관리”

발급:

Terminal window
# 도메인 소유권 확인 후 인증서 발급 (DNS 검증 방식)
aws acm request-certificate \
--domain-name "api.example.com" \
--subject-alternative-names "*.example.com" \
--validation-method DNS \
--region ap-northeast-2
# 검증용 CNAME 레코드 확인
aws acm describe-certificate \
--certificate-arn arn:aws:acm:ap-northeast-2:123456789:certificate/abc-123

ACM의 장점:

- 자동 갱신: 만료 60일 전 자동 갱신 (갱신 실패 알림 설정 권장)
- ALB/CloudFront에서 직접 참조 가능 (개인키 다운로드 불가 = 보안)
- 무료 (AWS 서비스 내에서 사용하는 경우)
- 와일드카드 인증서 지원

ACM 인증서 갱신 실패 대응:

Terminal window
# ACM 갱신 실패 알림 (EventBridge + SNS)
aws events put-rule \
--name "ACMCertExpiry" \
--event-pattern '{
"source": ["aws.acm"],
"detail-type": ["ACM Certificate Approaching Expiration"]
}'

ACM을 쓸 수 없는 환경(EC2 직접 노출, 온프레미스)에서 무료 인증서를 발급받는다.

Terminal window
# Certbot 설치 및 인증서 발급 (Nginx 자동 설정)
apt-get install certbot python3-certbot-nginx
certbot --nginx -d api.example.com -d www.example.com
# DNS 챌린지 방식 (와일드카드 인증서)
certbot certonly \
--dns-route53 \
-d "*.example.com" \
-d "example.com"
# 갱신 테스트
certbot renew --dry-run
# 자동 갱신 크론 (90일 유효기간, 30일 전 갱신)
echo "0 0,12 * * * root certbot renew --quiet" >> /etc/crontab

인증서 파일 구조:

/etc/letsencrypt/live/example.com/
├── cert.pem ← Leaf 인증서
├── chain.pem ← Intermediate CA 인증서
├── fullchain.pem ← cert.pem + chain.pem (Nginx에 이걸 설정)
└── privkey.pem ← 개인키 (절대 외부 노출 금지)

인증서 폐기 여부를 확인하는 두 가지 방법:

CRL (Certificate Revocation List):
- CA가 주기적으로 폐기 목록을 배포
- 클라이언트가 목록 전체를 다운로드 → 느리고 용량 큼
OCSP (Online Certificate Status Protocol):
- 클라이언트가 CA OCSP 서버에 실시간 질의
- 단점: 클라이언트의 방문 사실이 CA에 노출 (프라이버시)
CA 서버 장애 시 접속 불가
OCSP Stapling:
- 서버가 OCSP 응답을 미리 캐시하여 TLS 핸드셰이크에 포함
- 클라이언트는 CA에 직접 접속하지 않아도 됨
- Nginx에서: ssl_stapling on;

매번 풀 핸드셰이크를 하면 RTT 비용이 높다. 재연결 시 이전 세션을 재사용하는 두 가지 방식:

TLS 1.2 Session Ticket:
- 서버가 세션 상태를 암호화하여 클라이언트에 전달
- 재연결 시 클라이언트가 Ticket을 제출 → 1 RTT로 단축
- 문제: Ticket 암호화 키가 유출되면 과거 세션 복호화 가능
(Forward Secrecy 약화)
TLS 1.3 PSK (Pre-Shared Key):
- 이전 세션에서 Resumption Master Secret 파생
- 재연결 시 PSK Identity 제출 → 1 RTT (0-RTT도 가능)
- Forward Secrecy 유지

2013년 DigiNotar 사태 이후 도입된 공개 감사 시스템:

인증서 발급 시:
CA → CT Log 서버에 인증서 등록 (공개 로그)
→ SCT (Signed Certificate Timestamp) 수신
→ 인증서에 SCT 포함
브라우저 검증:
- 인증서에 SCT가 없으면 경고 표시
- 도메인 소유자가 CT 로그를 모니터링하여 불법 발급 감지

Terminal window
# 기본 TLS 연결 확인
openssl s_client -connect api.example.com:443
# 주요 출력 해석:
# Certificate chain: 인증서 체인 (0=Leaf, 1=Intermediate, 2=Root)
# Verify return code: 0 (ok) → 정상, 그 외는 에러
# Protocol: TLSv1.3 → 사용된 TLS 버전
# Cipher: TLS_AES_256_GCM_SHA384 → 협상된 암호 suite
# TLS 버전 강제 지정
openssl s_client -connect api.example.com:443 -tls1_2
openssl s_client -connect api.example.com:443 -tls1_3
# SNI 명시 (IP로 접속 시)
openssl s_client -connect 54.1.2.3:443 -servername api.example.com
# 인증서 만료일 확인
openssl s_client -connect api.example.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
# 인증서 체인 전체 출력
openssl s_client -connect api.example.com:443 -showcerts </dev/null
# mTLS 클라이언트 인증서 제시
openssl s_client -connect internal-api:8443 \
-cert client.crt \
-key client.key \
-CAfile ca.crt
# STARTTLS (이메일 서버)
openssl s_client -connect smtp.example.com:587 -starttls smtp

자주 보는 에러 코드:

Verify return code: 0 (ok) → 정상
Verify return code: 10 (certificate has expired) → 인증서 만료
Verify return code: 18 (self signed certificate) → 자체 서명 (신뢰 안됨)
Verify return code: 19 (self signed cert in chain) → 체인에 자체 서명 CA
Verify return code: 20 (unable to get local issuer cert) → Intermediate CA 누락
Verify return code: 21 (unable to verify the first cert) → Root CA 신뢰 저장소 부재
Terminal window
# TLS 핸드셰이크 상세 출력
curl -v https://api.example.com/health
# -v 출력에서 TLS 관련 라인:
# * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
# * Server certificate:
# * subject: CN=api.example.com
# * start date: Jan 1 00:00:00 2025 GMT
# * expire date: Feb 1 00:00:00 2026 GMT
# * subjectAltName: host "api.example.com" matched cert's "api.example.com"
# * issuer: CN=Amazon RSA 2048 M02
# * SSL certificate verify ok.
# 인증서 검증 무시 (개발/테스트 환경, 절대 프로덕션 금지)
curl -k https://self-signed.example.com
# mTLS 클라이언트 인증
curl --cert client.crt --key client.key --cacert ca.crt \
https://internal-api:8443/health
# TLS 버전 강제
curl --tlsv1.3 https://api.example.com
# 특정 암호 suite 사용
curl --ciphers ECDHE-RSA-AES256-GCM-SHA384 https://api.example.com
# 응답 시간 분석 (TCP connect + TLS handshake 분리)
curl -o /dev/null -s -w "
namelookup: %{time_namelookup}s
connect: %{time_connect}s
appconnect: %{time_appconnect}s ← TLS 핸드셰이크 완료 시점
pretransfer: %{time_pretransfer}s
total: %{time_total}s
" https://api.example.com

TLS 핸드셰이크 시간 = time_appconnect - time_connect

5-3. AWS 환경에서의 인증서 관리 패턴

섹션 제목: “5-3. AWS 환경에서의 인증서 관리 패턴”
┌─────────────────────────────────────────────────────┐
│ 공개 도메인 (Internet-facing) │
│ │
│ Route 53 → ALB (ACM 인증서) → ECS/EKS │
│ - ACM 자동 갱신 (관리 불필요) │
│ - ALB가 TLS 종료 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 내부 서비스 간 통신 (Private) │
│ │
│ Service A → Service B (mTLS) │
│ - AWS Private CA (사설 Root CA 관리) │
│ - 또는 Istio의 Citadel이 SVID 자동 발급 │
│ - SPIFFE/SPIRE 프레임워크 활용 │
└─────────────────────────────────────────────────────┘

AWS Private CA로 내부 mTLS:

Terminal window
# Private CA 생성
aws acm-pca create-certificate-authority \
--certificate-authority-configuration \
KeyAlgorithm=RSA_2048,\
SigningAlgorithm=SHA256WITHRSA,\
Subject="{Country=KR,Organization=MyCompany,CommonName=Internal CA}" \
--certificate-authority-type ROOT
# 서비스 인증서 발급
aws acm-pca issue-certificate \
--certificate-authority-arn arn:aws:acm-pca:... \
--csr fileb://service.csr \
--signing-algorithm SHA256WITHRSA \
--validity Value=365,Type=DAYS
Terminal window
# cert-manager 설치 (Let's Encrypt 자동화)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# ClusterIssuer 설정 (Let's Encrypt)
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
# Ingress에 TLS 자동 발급 지정
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.example.com
secretName: api-tls-secret # cert-manager가 자동 생성
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
EOF

6. 프론트엔드 → 플랫폼 브릿지

섹션 제목: “6. 프론트엔드 → 플랫폼 브릿지”

6-1. 브라우저 자물쇠 아이콘의 의미

섹션 제목: “6-1. 브라우저 자물쇠 아이콘의 의미”

프론트 개발자로서 자물쇠 아이콘을 “HTTPS 연결됨”으로 이해했다면, 플랫폼 엔지니어로서는 그 뒤에서 일어나는 과정을 인프라 관점에서 읽어야 한다.

사용자가 https://app.example.com을 입력할 때 일어나는 일:
1. DNS 조회
→ Route 53: app.example.com → ALB IP
2. TCP 연결 (3-way handshake)
→ 클라이언트 ↔ ALB
3. TLS 핸드셰이크
→ ALB가 ACM에서 발급받은 *.example.com 인증서 제시
→ 클라이언트: SNI로 app.example.com 인증서 요청
→ 클라이언트: 인증서 체인 검증 (Amazon Root CA → Amazon Intermediate → Leaf)
→ ECDHE 키 교환으로 세션 키 생성
→ 총 소요: ~50-100ms (리전 간 latency 포함)
4. HTTP 요청 (암호화됨)
→ ALB: TLS 종료 → HTTP로 변환
→ ALB → Target Group(ECS Task)으로 라우팅
→ X-Forwarded-Proto: https 헤더 추가
5. 브라우저 표시
→ 자물쇠 아이콘: 인증서 유효 + 혼합 콘텐츠(Mixed Content) 없음

6-2. 자물쇠가 깨질 때 (Mixed Content)

섹션 제목: “6-2. 자물쇠가 깨질 때 (Mixed Content)”
// ❌ HTTPS 페이지에서 HTTP 리소스 로드 → 자물쇠 아이콘 경고
<img src="http://cdn.example.com/logo.png" />
<script src="http://analytics.example.com/tracker.js"></script>
// ✓ 해결: 프로토콜 상대 URL 또는 HTTPS 강제
<img src="https://cdn.example.com/logo.png" />
<img src="//cdn.example.com/logo.png" /> // 현재 프로토콜 따라감

Content-Security-Policy로 강제 업그레이드:

Content-Security-Policy: upgrade-insecure-requests

6-3. HSTS (HTTP Strict Transport Security)

섹션 제목: “6-3. HSTS (HTTP Strict Transport Security)”

첫 번째 HTTP 요청을 허용하면 공격자가 중간에서 가로챌 수 있다(SSL Strip 공격). HSTS는 브라우저에 “이 도메인은 앞으로 무조건 HTTPS만 써라”고 지시한다.

응답 헤더:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age=63072000 → 2년간 기억
includeSubDomains → 서브도메인도 포함
preload → HSTS Preload List에 등록 (브라우저 배포판에 하드코딩)

HSTS Preload List: chromium.org/hsts에 등록하면 Chrome, Firefox, Safari 등 모든 주요 브라우저가 해당 도메인을 처음부터 HTTPS로만 접속한다. 한 번 등록하면 제거가 어려우므로 신중하게 결정해야 한다.

6-4. 인증서 문제 → 사용자 영향 시뮬레이션

섹션 제목: “6-4. 인증서 문제 → 사용자 영향 시뮬레이션”
시나리오: ACM 자동 갱신 실패로 인증서 만료
사용자 경험:
- Chrome: "연결이 비공개로 설정되어 있지 않습니다" (ERR_CERT_DATE_INVALID)
- Safari: "이 연결은 개인 정보 보호를 위해 안전하지 않습니다"
- curl: SSL certificate problem: certificate has expired
모니터링 설정 (CloudWatch + Lambda):
- ACM 만료 45일 전 EventBridge 알림 트리거
- Prometheus + Blackbox Exporter: ssl_cert_expiry 메트릭 수집
- PagerDuty 연동: 30일 미만 Critical, 60일 미만 Warning
Terminal window
# 인증서 만료일 모니터링 스크립트
check_cert_expiry() {
local domain=$1
local threshold_days=${2:-30}
expiry=$(openssl s_client -connect ${domain}:443 -servername ${domain} \
</dev/null 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
expiry_epoch=$(date -d "${expiry}" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "${domain}: ${days_left}일 남음 (만료: ${expiry})"
if [ $days_left -lt $threshold_days ]; then
echo "WARNING: 갱신 필요!"
return 1
fi
}
check_cert_expiry api.example.com 30

🔧 “certificate has expired” — ACM 자동 갱신 실패

섹션 제목: “🔧 “certificate has expired” — ACM 자동 갱신 실패”

증상:

curl: (60) SSL certificate problem: certificate has expired
# 또는 브라우저: ERR_CERT_DATE_INVALID
# openssl 확인:
openssl s_client -connect api.example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates
# notAfter=Mar 1 00:00:00 2025 GMT ← 현재 날짜 이후면 만료

원인: ACM은 도메인 소유권 검증(DNS CNAME)이 유지되어야 자동 갱신된다. Route 53에서 CNAME 레코드를 실수로 삭제하거나, 외부 DNS로 마이그레이션 시 검증 레코드를 누락하면 갱신 실패 → 만료로 이어진다.

해결:

  1. ACM Console에서 인증서 상태 확인: Pending validation → DNS CNAME 레코드 누락
  2. Route 53에 ACM이 요구하는 CNAME 레코드 재추가
  3. 즉시 새 인증서 요청 및 ALB 리스너에 교체
  4. 재발 방지: EventBridge 규칙으로 만료 45일 전 알림 설정
Terminal window
aws events put-rule --name "ACMExpiryAlert" \
--event-pattern '{"source":["aws.acm"],"detail-type":["ACM Certificate Approaching Expiration"]}'

🔧 “unable to get local issuer certificate” — Intermediate CA 누락

섹션 제목: “🔧 “unable to get local issuer certificate” — Intermediate CA 누락”

증상:

Terminal window
openssl s_client -connect api.example.com:443
# Verify return code: 20 (unable to get local issuer certificate)
# Certificate chain:
# 0 s:CN=api.example.com ← Leaf만 있고 Intermediate 없음

브라우저에서는 일부 클라이언트(특히 구버전 Android, curl)에서 연결 실패.

원인: Nginx/Apache에 ssl_certificatecert.pem(Leaf만)으로 설정했을 때 발생. Intermediate CA 인증서를 포함한 fullchain.pem을 사용해야 한다. Let’s Encrypt 환경에서 자주 발생.

해결:

Terminal window
# 잘못된 설정
ssl_certificate /etc/nginx/certs/cert.pem;
# 올바른 설정 (Intermediate CA 포함)
ssl_certificate /etc/nginx/certs/fullchain.pem;
# 검증: 체인이 완전한지 확인
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null \
| grep "s:CN"
# 출력 예시 (정상):
# 0 s:CN=api.example.com
# 1 s:CN=R11, O=Let's Encrypt, C=US ← Intermediate
# 2 s:CN=ISRG Root X1 ← Root (선택적)

🔧 ALB mTLS에서 “403 Forbidden” — 클라이언트 인증서 미제출

섹션 제목: “🔧 ALB mTLS에서 “403 Forbidden” — 클라이언트 인증서 미제출”

증상:

HTTP/2 403
x-amzn-mtls-clientcert-validity: FAILED
# 또는 curl:
curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure

원인: ALB에 mutual-authentication Mode=verify가 설정된 리스너에 클라이언트 인증서 없이 접속했거나, TrustStore에 없는 CA가 서명한 인증서를 제출했을 때 발생. 서비스 간 통신에서 새 마이크로서비스를 추가하면서 인증서 프로비저닝을 빠뜨린 경우에 흔하다.

해결:

Terminal window
# 1. 클라이언트 인증서 첨부하여 재시도
curl --cert client.crt --key client.key --cacert ca.crt \
https://internal-api.example.com/health
# 2. ALB Trust Store 확인
aws elbv2 describe-trust-stores \
--trust-store-arns arn:aws:elasticloadbalancing:...
# CA 번들 업데이트 시:
aws elbv2 modify-trust-store \
--trust-store-arn arn:aws:elasticloadbalancing:... \
--ca-certificates-bundle-s3-bucket my-bucket \
--ca-certificates-bundle-s3-key ca-bundle.pem
# 3. 인증서 발급 CA 확인
openssl x509 -in client.crt -noout -issuer
# issuer=CN=Internal CA, O=MyCompany ← TrustStore의 CA와 일치해야 함

🔧 TLS 핸드셰이크 타임아웃 — SNI 불일치

섹션 제목: “🔧 TLS 핸드셰이크 타임아웃 — SNI 불일치”

증상:

Terminal window
curl -v https://54.123.45.67/api
# * SSL: no alternative certificate subject name matches target host name '54.123.45.67'
# curl: (51) SSL: no alternative certificate subject name matches target host name

또는 ALB 액세스 로그에서 ssl_error 필드에 HANDSHAKE_FAILURE.

원인: IP 주소로 직접 접속하거나 SNI를 지원하지 않는 구형 클라이언트가 잘못된 인증서를 받을 때 발생. ALB에 기본(default) 인증서가 없거나 잘못 설정된 경우도 해당.

해결:

Terminal window
# SNI 명시적 지정
curl -v --resolve api.example.com:443:54.123.45.67 https://api.example.com/api
# 또는 openssl로 SNI 지정
openssl s_client -connect 54.123.45.67:443 -servername api.example.com
# ALB 기본 인증서 확인
aws elbv2 describe-listeners --load-balancer-arn arn:aws:elasticloadbalancing:... \
| jq '.Listeners[].Certificates'

TLS의 세 보장: 암호화(AES) + 무결성(HMAC) + 인증(X.509)
핸드셰이크 흐름:
TLS 1.2: 2 RTT → ClientHello → ServerHello+Cert → KeyExchange → Finished
TLS 1.3: 1 RTT → ClientHello(key_share) → ServerHello+Cert+Finished → Finished
키 역할:
비대칭키 → 핸드셰이크에서 세션 키 교환
대칭키 → 실제 데이터 암호화
인증서 체인: Root CA → Intermediate → Leaf
SNI: ClientHello에 hostname 포함 → 하나의 IP에서 다수 도메인 지원
mTLS: 클라이언트도 인증서 제시 → 서비스 간 상호 인증
TLS Termination: ALB(ACM) → 백엔드 HTTP / Nginx → 백엔드 HTTP
  • ALB 리스너에 TLS 1.2 이상만 허용 (SSL Policy: ELBSecurityPolicy-TLS13-1-2-2021-06)
  • ACM 인증서 만료 알림 설정 (EventBridge → SNS)
  • HSTS 헤더 설정 (max-age 최소 1년)
  • 내부 서비스 간 mTLS 또는 서비스 메시 적용 검토
  • TLS 1.0 / 1.1 비활성화 확인
  • 인증서 체인 완전성 확인 (openssl s_client -showcerts)
  • Mixed Content 경고 제거
  • OCSP Stapling 활성화 (Nginx 사용 시)
  • 와일드카드 인증서와 SAN 인증서 용도 구분

8. 참고: 주요 알고리즘 용어 정리

섹션 제목: “8. 참고: 주요 알고리즘 용어 정리”
ECDHE Elliptic Curve Diffie-Hellman Ephemeral
타원곡선 기반 키 교환. "Ephemeral"이므로 Forward Secrecy 보장
AES-256-GCM Advanced Encryption Standard 256-bit, Galois/Counter Mode
대칭 암호화. GCM은 AEAD로 암호화+무결성 동시 제공
SHA-256 Secure Hash Algorithm 256-bit
서명 해시 함수. 인증서 서명에 사용
RSA Rivest–Shamir–Adleman
비대칭 암호화. 인증서 공개키/개인키에 사용
현재는 2048-bit 이상 권장, 4096-bit 사용 증가
ECDSA Elliptic Curve Digital Signature Algorithm
RSA 대비 짧은 키로 동일한 보안 수준 (EC 256-bit ≈ RSA 3072-bit)
X.509 인증서 표준 형식. PEM(.pem, .crt), DER(.der) 등 인코딩 방식이 다름
PEM Privacy Enhanced Mail
Base64 인코딩 + -----BEGIN CERTIFICATE----- 헤더
Nginx, OpenSSL에서 주로 사용
PKCS#12 .p12 / .pfx 확장자. 인증서 + 개인키를 하나의 파일로 묶음
Java KeyStore, Windows에서 주로 사용


Terminal window
# 1. TLS 버전 및 암호화 suite 확인
openssl s_client -connect google.com:443 </dev/null 2>/dev/null \
| grep -E "Protocol|Cipher"

예상 출력:

Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Terminal window
# 2. 인증서 만료일 확인
openssl s_client -connect google.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates

예상 출력:

notBefore=Jan 13 08:21:32 2025 GMT
notAfter=Apr 7 08:21:31 2025 GMT
Terminal window
# 3. 인증서 체인 전체 확인
openssl s_client -connect google.com:443 -showcerts </dev/null 2>/dev/null \
| grep -E "^(depth|verify|s:|i:)"

예상 출력:

depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=*.google.com
verify return:1
Terminal window
# 4. curl로 TLS 핸드셰이크 시간 측정
curl -o /dev/null -s -w \
"connect: %{time_connect}s | TLS done: %{time_appconnect}s | total: %{time_total}s\n" \
https://google.com

예상 출력:

connect: 0.025143s | TLS done: 0.071832s | total: 0.098421s
# TLS 핸드셰이크: 0.071832 - 0.025143 = 약 47ms
Terminal window
# 5. SNI 없이 접속 시 어떤 인증서가 오는지 확인
openssl s_client -connect $(dig +short api.example.com | head -1):443 </dev/null 2>/dev/null \
| openssl x509 -noout -subject
# SNI 없이 IP 직접 접속 → ALB 기본(default) 인증서 반환
Terminal window
# 6. ACM 인증서 목록 확인 (AWS CLI)
aws acm list-certificates --region ap-northeast-2 \
--query 'CertificateSummaryList[*].{Domain:DomainName,Status:Status,Expiry:NotAfter}' \
--output table

예상 출력:

------------------------------------------------------------
| ListCertificates |
+-----------------------------+-----------+----------------+
| Domain | Status | Expiry |
+-----------------------------+-----------+----------------+
| *.example.com | ISSUED | 2026-02-01 |
| api.internal.example.com | ISSUED | 2026-01-15 |
+-----------------------------+-----------+----------------+

TLS는 비대칭키로 세션 키를 교환하고, 대칭키로 실제 데이터를 암호화하며, X.509 인증서 체인으로 서버 신원을 검증하는 3단계 보안 프로토콜이다. TLS 1.3은 이 과정을 1 RTT로 단축하고 Forward Secrecy를 강제화하여 성능과 보안을 동시에 향상시켰다.