certbot 갱신 실패 해결: certificate expired 원인별 트러블슈팅 가이드
새벽에 Your certificate (...) will expire in 0 days 메일을 받았다면, 일단 커피보다 터미널을 먼저 여세요. 사이트가 죽기 전에 쳐야 할 첫 번째 명령어는 단 하나입니다.
sudo certbot renew --dry-run이 명령은 실제 인증서를 건드리지 않고 갱신 과정을 그대로 시뮬레이션합니다. 여기서 나는 에러 메시지가 곧 진짜 갱신이 실패하는 이유입니다. 이 글은 그 메시지를 원인별로 매칭해 복붙 명령어로 해결하는 실전 가이드입니다.
참고로, 갱신은 성공했는데 클라이언트(curl, 자바 앱 등)에서 인증서 신뢰 오류가 난다면 이 글이 아니라 'unable to get local issuer certificate' 글을 봐야 합니다. 이 글은 서버측 인증서 갱신 문제를, 그 글은 클라이언트측 CA 체인 검증 문제를 다룹니다. 둘은 증상이 비슷해 보여도 원인이 완전히 다릅니다.
1단계: dry-run으로 실패를 미리 재현하고 로그 읽기
먼저 현재 인증서 상태와 만료일을 확인합니다.
sudo certbot certificates출력에서 Expiry Date와 VALID: N days 부분을 봅니다. 만료가 임박했다면 곧장 --dry-run을 돌리고, 실패 시 로그를 봅니다.
# 가장 최근 시도의 상세 로그
sudo tail -n 50 /var/log/letsencrypt/letsencrypt.log
# snap 기반 certbot은 timer 로그를 journalctl로
sudo journalctl -u certbot --since "1 hour ago"최근 certbot은 snap 기본 배포로 바뀌면서 바이너리 경로가 /snap/bin/certbot인 경우가 많고, 인증서는 ECDSA가 기본이 됐습니다. 로그 위치(/var/log/letsencrypt/)는 동일하니 여기를 먼저 보면 됩니다. 로그에서 핵심 키워드 한 줄만 잡으면 아래 원인 매칭으로 바로 넘어갈 수 있습니다.
2단계: 원인별 해결 백과 (복붙 명령어)
원인 ① 포트 80 차단 (HTTP-01 challenge)
로그에 이런 메시지가 보이면 방화벽/보안그룹 문제입니다.
Timeout during connect (likely firewall problem)HTTP-01 챌린지는 Let's Encrypt 서버가 외부에서 당신의 80번 포트로 접속해 검증합니다. 막혀 있으면 무조건 실패합니다.
# 80포트를 누가 듣고 있는지
sudo ss -tlnp | grep :80
# OS 방화벽 허용
sudo ufw allow 80/tcp
# 클라우드(AWS/GCP/오라클)라면 보안그룹/네트워크 ACL 인바운드 80 허용도 반드시 확인특히 클라우드 인스턴스는 OS 방화벽을 열어도 보안그룹 인바운드가 막혀 있으면 똑같이 timeout이 납니다. 양쪽 다 보세요.
원인 ② nginx/apache 플러그인 충돌
플러그인 선택 기준부터 정리합니다.
| 상황 | 권장 옵션 |
|---|---|
| nginx 운영 중, 설정 자동 수정 OK | --nginx |
| 웹서버는 그대로, 파일만 검증 | --webroot -w /var/www/html |
| 웹서버 없음 / 임시 검증 | --standalone |
--standalone은 자체적으로 80포트를 띄우는데, 이미 nginx가 80을 점유하면 충돌합니다. 이때는 잠깐 멈췄다 켜는 hook을 씁니다.
sudo certbot certonly --standalone \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx" \
-d example.com원인 ③ DNS-01 검증 실패
와일드카드(*.example.com) 인증서는 DNS-01이 필수입니다. TXT 레코드 전파가 안 됐을 때 실패합니다.
# _acme-challenge TXT 레코드 전파 확인
dig TXT _acme-challenge.example.com +shortDNS 플러그인을 쓴다면 자격증명 파일 권한이 흔한 함정입니다.
# cloudflare 예시: 자격증명 파일 권한은 600 이어야 함
chmod 600 ~/.secrets/cloudflare.ini
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d "*.example.com" -d example.com원인 ④ rate limit 초과
로그에 다음이 보이면 한도 초과입니다.
too many certificates already issuedLet's Encrypt는 동일 등록 도메인당 주 50건, 중복 인증서 주 5건 한도가 있습니다. 실패를 반복하며 한도를 까먹지 않으려면 반드시 staging에서 먼저 테스트하세요.
sudo certbot certonly --staging -d example.comstaging은 별도 한도라 마음껏 테스트할 수 있습니다. 정상 동작을 확인한 뒤 --staging만 빼고 실행하세요. 한도가 찼다면 보통 해소까지 최대 1주 기다려야 하므로, 그 전에 테스트로 한도를 태우는 실수를 피하는 게 핵심입니다.
원인 ⑤ timer/cron 미동작으로 인한 만료
가장 흔한 "조용한 사망" 패턴입니다. 명령어는 잘 되는데 자동갱신이 안 돌아 만료된 경우죠.
# systemd timer 확인
systemctl list-timers | grep certbot
systemctl status certbot.timer
# cron 환경이라면
crontab -lcertbot.timer가 inactive거나 list-timers에 안 보이면 자동갱신이 멈춘 겁니다.
sudo systemctl enable --now certbot.timer3단계: 재발 방지 — deploy-hook으로 nginx reload 자동화
실무에서 제가 가장 많이 본 함정은 이겁니다. 갱신은 성공했는데 브라우저는 여전히 "만료"라고 표시되는 경우. 원인은 단순합니다. nginx가 옛 인증서를 메모리에 들고 있어서 reload가 안 된 것이죠.
갱신 성공 시에만 nginx를 reload하도록 deploy-hook을 겁니다.
sudo certbot renew --deploy-hook "systemctl reload nginx"이미 발급된 도메인이라면 /etc/letsencrypt/renewal/<domain>.conf에 한 줄을 영구 등록하는 게 깔끔합니다.
# /etc/letsencrypt/renewal/example.com.conf
renew_hook = systemctl reload nginxACME 단기 인증서(6일 수명) 옵션과 OCSP stapling 폐기 흐름 때문에, 앞으로 자동갱신 안정성은 더 중요해집니다. 갱신 주기가 짧아질수록 hook 누락 한 번이 곧 장애로 이어지니, deploy-hook은 선택이 아니라 필수라고 보시면 됩니다.
4단계: 만료 D-1 긴급 발급 절차
내일 만료인데 자동갱신이 깨졌다면, 다운타임을 최소화하며 강제 발급합니다.
sudo certbot certonly --standalone \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx" \
-d example.com --force-renewal--force-renewal은 남은 기간과 무관하게 새 인증서를 받습니다. nginx 중단 시간은 보통 수 초이니 트래픽이 적은 시간대를 노리거나, 가능하면 --webroot로 무중단 발급하세요.
결론: 복붙 체크리스트
certbot renew --dry-run실행해 에러 메시지 확보/var/log/letsencrypt/letsencrypt.log에서 키워드 매칭- timeout → 80포트/보안그룹, "too many certificates" → rate limit/staging
- DNS-01 →
dig TXT _acme-challenge..., 자격증명 권한 600 systemctl list-timers | grep certbot로 자동갱신 점검- deploy-hook으로 nginx reload 자동화
마지막으로 다시 한번. 갱신은 분명 성공했는데 클라이언트에서 신뢰 오류가 난다면, 그건 서버측 갱신 문제가 아니라 클라이언트의 CA 체인 검증 문제입니다. 이 경우 'unable to get local issuer certificate' 글을 참고하세요.
자주 묻는 질문 (FAQ)
Q. dry-run은 성공하는데 실제 갱신만 실패합니다. 왜죠?
A. dry-run은 staging 서버를 쓰기 때문에 rate limit과 무관합니다. 실제 갱신만 실패하면 운영 환경의 rate limit 초과(too many certificates already issued)일 확률이 높습니다. 로그를 확인하고 해소까지 대기하세요.
Q. 갱신은 됐는데 브라우저는 여전히 만료라고 뜹니다.
A. nginx/apache가 옛 인증서를 메모리에 들고 있어서입니다. systemctl reload nginx를 수동 실행하고, 재발 방지를 위해 renew_hook = systemctl reload nginx를 등록하세요.
Q. 여러 도메인 중 하나만 갱신에 실패합니다.
A. 해당 도메인만 DNS A 레코드가 다른 서버를 가리키거나, 80포트 검증 경로가 막혀 있을 가능성이 큽니다. 실패한 도메인으로 dig와 curl -I http://도메인/.well-known/acme-challenge/test를 개별 확인하세요.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...