SSL/TLS 실전 고수 팁
인증서 발급과 HTTPS 설정이 완료됐다고 끝이 아닙니다. SSL Labs A+ 등급 달성, 인증서 만료 전 자동 알림, 배포 자동화까지 갖춰야 진정한 프로덕션 수준의 HTTPS 운영이 됩니다.
SSL Labs A+ 등급 완전 달성 가이드
SSL Labs에서 A+ 등급을 받는 완전한 Nginx 설정입니다.
# /etc/nginx/conf.d/ssl-aplus.conf
# 전역 설정 (http 블록)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HTTPS 서버
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com www.example.com;
# 인증서 (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# TLS 1.2 + 1.3만 허용
ssl_protocols TLSv1.2 TLSv1.3;
# 안전한 암호 스위트 (ECDHE/DHE only)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:'
'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:'
'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:'
'DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS (A+ 핵심!)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 나머지 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server_tokens off;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
인증서 만료 모니터링 자동화
셸 스크립트 모니터링
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAINS=("example.com" "api.example.com" "admin.example.com")
ALERT_DAYS=30 # 이 일수 이하로 남으면 알림
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
for domain in "${DOMAINS[@]}"; do
# 인증서 만료일 조회
expiry=$(echo | openssl s_client -servername "$domain" \
-connect "$domain:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
if [ -z "$expiry" ]; then
send_slack "❌ [$domain] SSL 인증서 조회 실패!"
continue
fi
# 남은 일수 계산
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -le "$ALERT_DAYS" ]; then
message="⚠️ [$domain] SSL 인증서가 ${days_left}일 후 만료됩니다! (만료: $expiry)"
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
--data "{\"text\":\"$message\"}"
else
echo "✅ $domain: ${days_left}일 남음 ($expiry)"
fi
done
# crontab -e 에 추가 (매일 오전 9시 확인)
0 9 * * * /usr/local/bin/check-ssl-expiry.sh >> /var/log/ssl-check.log 2>&1
Prometheus + Blackbox Exporter로 자동 수집
# prometheus.yml
scrape_configs:
- job_name: 'ssl-expiry'
metrics_path: /probe
params:
module: [https_2xx]
static_configs:
- targets:
- https://example.com
- https://api.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# Grafana 알림 조건 (30일 이내 만료)
probe_ssl_earliest_cert_expiry - time() < 30 * 24 * 3600
Let's Encrypt 자동 갱신 검증
# 갱신 시뮬레이션 (실제 갱신 없이 테스트)
sudo certbot renew --dry-run
# 특정 도메인만 강제 갱신
sudo certbot renew --force-renewal -d example.com
# 갱신 후 Nginx 자동 재로드 (post-hook)
sudo certbot renew \
--post-hook "systemctl reload nginx" \
--pre-hook "nginx -t"
# certbot 타이머 로그 확인
sudo journalctl -u certbot.timer -n 50
갱신 실패 알림 설정
# /etc/letsencrypt/renewal-hooks/deploy/notify-renewal.sh
#!/bin/bash
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK"
DOMAINS=$RENEWED_DOMAINS
EXPIRY=$RENEWED_LINEAGE
curl -s -X POST "$SLACK_WEBHOOK" \
-d "{\"text\":\"✅ SSL 인증서 갱신 완료: $DOMAINS\"}"
# 실패 시 알림을 위한 cron 래퍼
#!/bin/bash
output=$(certbot renew 2>&1)
if [ $? -ne 0 ]; then
curl -X POST "$SLACK_WEBHOOK" \
-d "{\"text\":\"❌ SSL 인증서 갱신 실패!\n$output\"}"
fi
흔한 SSL 설정 실수
실수 1: 인증서 체인 불완전
# 체인 확인
openssl s_client -connect example.com:443 -showcerts 2>/dev/null \
| grep -E "s:|i:"
# s: = 현재 인증서
# i: = 발급자 (중간 CA)
# 마지막 i:가 Root CA여야 함
# fullchain.pem 사용하면 해결
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # ✅
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem; # ❌ 체인 누락
실수 2: 개인키 권한 설정
# 개인키는 root만 읽을 수 있어야 함
sudo chmod 600 /etc/letsencrypt/live/example.com/privkey.pem
sudo chown root:root /etc/letsencrypt/live/example.com/privkey.pem
# Nginx는 root로 실행되므로 접근 가능
실수 3: Mixed Content (HTTPS 페이지에서 HTTP 리소스)
# 개발자 도구 콘솔에서 확인
# Mixed Content: The page was loaded over HTTPS, but requested an insecure resource
# CSP 헤더로 감지
add_header Content-Security-Policy-Report-Only "default-src https:; report-uri /csp-report";
# 해결: 모든 리소스 URL을 https:// 또는 //로 변경
# <img src="http://cdn.example.com/logo.png"> ❌
# <img src="https://cdn.example.com/logo.png"> ✅
# <img src="//cdn.example.com/logo.png"> ✅ (프로토콜 상속)
실수 4: HSTS 설정 후 HTTP 서비스 필요
HSTS max-age를 설정한 후 일시적으로 HTTP가 필요한 상황이 발생하면
브라우저가 해당 기간 동안 HTTPS만 요청합니다.
해결: 초기에는 max-age=300(5분)으로 테스트하고
문제 없으면 max-age=63072000(2년)으로 증가시킵니다.
배포 자동화 체크리스트
#!/bin/bash
# ssl-deploy-check.sh — 배포 전 SSL 설정 자동 검증
DOMAIN=$1
echo "=== SSL Configuration Check for $DOMAIN ==="
# 1. 인증서 유효성
expiry=$(echo | openssl s_client -servername "$DOMAIN" \
-connect "$DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate)
echo "✅ Certificate: $expiry"
# 2. TLS 버전 확인
tls13=$(openssl s_client -connect "$DOMAIN:443" -tls1_3 2>/dev/null | grep "Protocol")
echo "✅ TLS 1.3: $tls13"
# 3. TLS 1.0/1.1 차단 확인
tls10=$(openssl s_client -connect "$DOMAIN:443" -tls1 2>&1 | grep -c "handshake failure")
[ "$tls10" -eq 1 ] && echo "✅ TLS 1.0 blocked" || echo "❌ TLS 1.0 NOT blocked"
# 4. HSTS 헤더 확인
hsts=$(curl -sI "https://$DOMAIN" | grep -i "strict-transport")
[ -n "$hsts" ] && echo "✅ HSTS: $hsts" || echo "❌ HSTS missing"
# 5. OCSP Stapling
stapling=$(echo | openssl s_client -connect "$DOMAIN:443" -status 2>/dev/null | grep -c "OCSP Response Status")
[ "$stapling" -ge 1 ] && echo "✅ OCSP Stapling enabled" || echo "⚠️ OCSP Stapling not confirmed"
echo "=== Check complete ==="
SSL 성능 최적화 요약
| 설정 | 효과 | Nginx 지시어 |
|---|---|---|
| SSL 세션 캐시 | 재연결 핸드셰이크 생략 | ssl_session_cache shared:SSL:10m |
| TLS 1.3 | 핸드셰이크 1-RTT | ssl_protocols TLSv1.3 |
| OCSP Stapling | 인증서 확인 지연 제거 | ssl_stapling on |
| HTTP/2 | 다중화, 헤더 압축 | http2 on |
| ECDSA 인증서 | RSA보다 빠름 | 인증기관에서 EC 키 선택 |