무중단 HTTPS 유지: Let’s Encrypt 자동갱신 안정화(이중 인증서, 모니터링, 무중단 교체)
무료 SSL을 쓰는 이유는 분명하지만, 갱신 실패 한 번이면 곧바로 사용자에게 빨간 경고가 뜹니다. 이 글은 Ubuntu 24.04 + Nginx + Certbot 기준으로 자동갱신 안정화에 초점을 맞춥니다. 핵심은 이중 인증서(RSA+ECDSA), deploy-hook로 무중단 reload, systemd 타이머 모니터링, 스테이징으로 사전 리허설, 알림/헬스체크입니다.
1) 현재 상태 점검(한 번만 확실히)
# 인증서 만료일/체인 점검(원격)
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | \
openssl x509 -noout -issuer -subject -dates
# 로컬 certbot 설치/버전
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
certbot --version
/etc/nginx에서 인증서 경로가 /etc/letsencrypt/live/<도메인>/를 가리키는지 확인하세요.
2) RSA + ECDSA 이중 인증서(호환성 + 성능)
신규 발급 또는 재발급 시 ECDSA(EC)와 RSA 두 가지를 모두 구성하면, 신형 클라이언트는 더 빠른 ECDSA를, 구형은 RSA를 사용합니다.
# 2개 인증서 각각 발급(HTTP-01, 웹서버가 80/443 소유)
sudo certbot certonly --nginx -d app.example.com --key-type ecdsa
sudo certbot certonly --nginx -d app.example.com --key-type rsa
Nginx 서버블록에서 인증서를 두 쌍 모두 선언합니다.
# /etc/nginx/sites-available/app.example.com (발췌)
server {
listen 443 ssl http2;
server_name app.example.com;
# ECDSA 우선
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# RSA 백업
ssl_certificate /etc/letsencrypt/live/app.example.com-0001/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com-0001/privkey.pem;
# OCSP 스테이플링 권장
ssl_stapling on; ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
location / {
proxy_pass http://127.0.0.1:8080;
# ...
}
}
-0001 경로나 실제 디렉터리명은 환경에 따라 달라집니다. ls /etc/letsencrypt/live로 정확히 확인 후 반영하세요.
3) 무중단 교체: deploy-hook로 안전하게 reload
갱신 직후 자동으로 Nginx를 reload해야 새 인증서가 적용됩니다.
certbot의 --deploy-hook 또는 갱신 구성 파일의 deploy-hook을 사용합니다.
# 1) 전역 훅 스크립트(재사용 가능)
sudo tee /usr/local/sbin/nginx-reload-on-renew.sh >/dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail
nginx -t
systemctl reload nginx
echo "$(date -Is) | nginx reloaded after cert renew" >> /var/log/certbot-deploy.log
SH
sudo chmod +x /usr/local/sbin/nginx-reload-on-renew.sh
# 2) 도메인별 갱신 설정에 deploy-hook 추가
# /etc/letsencrypt/renewal/app.example.com.conf (발췌)
# renew_hook = (구식) 대신
deploy_hook = /usr/local/sbin/nginx-reload-on-renew.sh
이제 certbot이 해당 인증서를 갱신하면 자동으로 Nginx 구성을 검사(nginx -t)하고 무중단 reload합니다.
4) systemd 타이머 확인 및 수동 리허설
snap 패키지의 certbot은 systemd 타이머로 하루 2회 갱신을 시도합니다.
systemctl status snap.certbot.renew.timer
journalctl -u snap.certbot.renew.service --since "2 days ago" --no-pager
리허설(스테이징): 실갱신 전 스테이징 CA로 동작 점검(레이트리밋 회피).
# 스테이징으로 강제 시뮬레이션(실제 신뢰 불가 인증서 발급)
sudo certbot renew --dry-run
# 특정 도메인만 강제 테스트(실갱신)
sudo certbot renew --cert-name app.example.com --force-renewal
--dry-run이 성공해야 밤중 자동갱신에 안심할 수 있습니다.
5) DNS-01(와일드카드)와 HTTP-01 혼합 전략
서브도메인이 많거나 와일드카드(*.example.com)가 필요하면 DNS-01이 필요합니다.
클라우드 DNS를 쓰면 API 토큰으로 자동화가 가능합니다.
# 예: Cloudflare DNS 플러그인(예시), 제공자마다 다름
sudo snap install certbot-dns-cloudflare
# /root/.secrets/cloudflare.ini에 API 토큰 저장(권한 600)
sudo certbot -d '*.example.com' -d example.com \
--dns-cloudflare --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
--preferred-challenges dns-01
프론트만 단일 호스트면 HTTP-01이 더 단순/안정적입니다. 혼용 시 문서화로 누가 어떤 인증서에 책임지는지 명확히 하세요.
6) 에러 방지 체크리스트(운영에서 자주 터지는 포인트)
- 포트/도메인: 80/443이 외부에서 도달 가능한가(방화벽/보안그룹/UFW/프록시 체인 포함).
- challenges 경로: Nginx가
/.well-known/acme-challenge/를 백엔드로 프록시하지 말고 직접 서빙.
# 챌린지 핸들러(공통 서버블록에 추가)
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type "text/plain";
try_files $uri =404;
}
# webroot 방식 사전 준비
sudo mkdir -p /var/www/certbot
sudo chown -R www-data:www-data /var/www/certbot
- 권한:
/etc/letsencrypt/live경로/심볼릭 링크 손상 금지(백업/복원 시 주의). - 레이트 리밋: 실패 반복 시 스테이징으로 전환해 원인 제거 후 본 발급.
- reload 누락:
deploy-hook를 반드시 등록해 갱신 직후 적용.
7) 보안/성능 추가 설정
# /etc/nginx/snippets/ssl-params.conf (예시)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_prefer_server_ciphers on;
# 서버 블록에서 include
include /etc/nginx/snippets/ssl-params.conf;
HSTS는 HTTPS가 완전히 안정화된 뒤 적용하세요.
# (선택) HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
8) 만료 임박 알림(메일/Slack)
만료 15일 전이면 알람이 울리도록 간단한 스크립트를 걸어두면 마음이 편합니다.
# /usr/local/sbin/cert-expiry-check.sh
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="app.example.com"
DAYS_LEFT=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2 | xargs -I{} date -d {} +%s)
NOW=$(date +%s)
LEFT=$(( (DAYS_LEFT - NOW) / 86400 ))
if [ "$LEFT" -lt 15 ]; then
echo "[ALERT] $DOMAIN cert expires in $LEFT days" | \
mail -s "TLS Expiry Alert: $DOMAIN" ops@example.com
fi
sudo chmod +x /usr/local/sbin/cert-expiry-check.sh
echo "0 9 * * * root /usr/local/sbin/cert-expiry-check.sh" | sudo tee /etc/cron.d/cert-expiry
Slack Webhook을 쓰면 팀 알림으로 바꿀 수도 있습니다.
9) 무중단 롤백 전략(비상시)
갱신 직후 특정 클라이언트에서 TLS 오류가 발생할 때를 대비해, 이전 체인을 보관하고 Nginx의 인증서 경로를 임시로 되돌릴 수 있어야 합니다.
# 직전 fullchain/privkey 백업(갱신 훅에서 자동 백업 가능)
sudo cp /etc/letsencrypt/live/app.example.com/fullchain.pem /etc/letsencrypt/backup/fullchain.$(date +%F).pem
sudo cp /etc/letsencrypt/live/app.example.com/privkey.pem /etc/letsencrypt/backup/privkey.$(date +%F).pem
문제가 확인되면 일시적으로 백업 경로를 가리키게 변경 후 nginx -t && systemctl reload nginx.
근본 원인을 찾은 뒤 다시 원위치하세요.
10) 전체 점검 루틴(복붙용)
# 타이머/최근 로그
systemctl status snap.certbot.renew.timer
journalctl -u snap.certbot.renew.service -n 100 --no-pager
# 인증서 만료일/체인
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | \
openssl x509 -noout -issuer -subject -dates
# Nginx 테스트/리로드
sudo nginx -t && sudo systemctl reload nginx
# (필요 시) 강제 갱신 및 무중단 적용
sudo certbot renew --cert-name app.example.com --force-renewal
11) 트러블슈팅 빠른 분기
- 챌린지 404 →
/.well-known/acme-challenge/가 프록시 뒤로 가지 않게 공용 루트에서 직접 서빙. - 권한/소유자 오류 →
/etc/letsencrypt는 root 소유, Nginx 접근은ssl_certificate경로만 읽기면 됨. - 체인 오류 →
fullchain.pem을 사용했는지 확인(서브신뢰 체인 포함). - 갱신 됐는데 브라우저는 예전 인증서 →
nginx reload누락 또는 중간 CDN/프록시 캐시 확인. - 레이트리밋 → 스테이징으로 원인 제거 후 본 발급 재시도.
12) 마무리
이중 인증서(RSA+ECDSA)와 deploy-hook 기반 무중단 reload, 타이머/로그 모니터링, 만료 알림만 갖춰도
Let’s Encrypt 운영 리스크는 크게 줄어듭니다. 오늘 dry-run과 deploy-hook부터 적용하고,
만료 15일 전 알림을 더하면 밤중 만료 사고를 막을 수 있습니다.
'서버 인프라 실무' 카테고리의 다른 글
| 트래픽 급증 대응 전략: 캐시·CDN·로드밸런서 실무 가이드 (0) | 2025.11.22 |
|---|---|
| 운영 자동화 — 배포와 로그를 하나로 묶는 DevOps 환경 만들기 실무 가이드 (0) | 2025.11.20 |
| 트레이스 기반 용량 계획: 스레드/커넥션 풀 USE 지표와 자동 스케일링 설계 (0) | 2025.10.30 |
| 트레이스 기반 SLO 구축: RED/USE 메트릭, 샘플링 전략, Grafana 경보까지 한 번에 (0) | 2025.10.30 |
| OpenTelemetry + Grafana Tempo로 분산 트레이싱 구축: Spring Boot 연동, OTel Collector, 지표·로그 연계 (1) | 2025.10.29 |