본문 바로가기
서버 인프라 실무

무중단 HTTPS 유지: Let’s Encrypt 자동갱신 안정화(이중 인증서, 모니터링, 무중단 교체)

by yamoojin83 2025. 11. 2.

무중단 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-rundeploy-hook부터 적용하고, 만료 15일 전 알림을 더하면 밤중 만료 사고를 막을 수 있습니다.