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 설치 기준, apt는 -u certbot.service)
sudo journalctl -u snap.certbot.renew.service --since "1 day ago"최근 certbot은 snap 기본 배포로 바뀌면서 바이너리 경로가 /snap/bin/certbot인 경우가 많고, 인증서는 ECDSA가 기본이 됐습니다. 로그 위치(/var/log/letsencrypt/)는 동일하니 여기를 먼저 보면 됩니다. 로그에서 핵심 키워드 한 줄만 잡으면 아래 원인 매칭으로 바로 넘어갈 수 있습니다.
브라우저나 certbot renew 출력에 뜬 문구로 원인을 먼저 좁힙니다.
| 보이는 문구 | 원인 | 바로 확인할 명령 | 해결 |
|---|---|---|---|
NET::ERR_CERT_DATE_INVALID (브라우저) | 서비스 중인 인증서가 이미 만료 | 아래 openssl s_client 확인 | 원인 제거 후 갱신, 웹서버 reload |
Timeout during connect (likely firewall problem) | 80 포트 차단(방화벽·보안그룹) | ss -tlnp | grep :80 | 원인 ① |
Connection refused | 80 포트에서 듣는 프로세스가 없음 | systemctl status nginx | 원인 ① |
Invalid response from http://.../.well-known/acme-challenge/... + 404 | webroot 경로가 실제 문서 루트와 다름 | nginx root / apache DocumentRoot | 원인 ②-1 |
DNS problem: NXDOMAIN looking up A / TXT | DNS 레코드·TXT 전파 문제 | dig +short example.com | 원인 ③ |
too many certificates already issued | rate limit 초과 | 대기, staging에서 테스트 | 원인 ④ |
갱신 결과와 별개로 지금 실제로 서비스되는 인증서를 확인하는 습관도 중요합니다. 파일은 새것인데 원격 결과가 옛날 만료일이면 웹서버 reload 문제입니다.
# 파일의 만료일
sudo openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/fullchain.pem
# 실제 서비스 중인 인증서의 유효기간
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates2단계: 원인별 해결 백과 (복붙 명령어)
원인 ① 포트 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원인 ②-1 webroot 경로 불일치 (404)
--webroot -w로 지정한 경로와 웹서버의 실제 문서 루트가 다르면 Let's Encrypt가 챌린지 파일을 찾지 못해 404가 납니다.
sudo nginx -T 2>/dev/null | grep -E '^\s*(server_name|root)' # nginx
sudo apachectl -S; grep -R "DocumentRoot" /etc/apache2/ /etc/httpd/ 2>/dev/null # apache
# 테스트 파일로 외부에서 실제로 보이는지 확인
sudo mkdir -p /var/www/html/.well-known/acme-challenge
echo ok | sudo tee /var/www/html/.well-known/acme-challenge/test
curl -i http://example.com/.well-known/acme-challenge/test| 항목 | nginx | apache |
|---|---|---|
| 자동 설정 플러그인 | --nginx | --apache |
| 문서 루트 설정 | server { root ... } | DocumentRoot |
| reload 명령 | systemctl reload nginx | systemctl reload apache2 (RHEL 계열은 httpd) |
플러그인(--nginx/--apache)은 인증서 설치와 reload까지 처리하지만, webroot/standalone으로 인증서만 받았다면 reload는 직접 걸어야 합니다.
원인 ③ 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 미동작으로 인한 만료
가장 흔한 "조용한 사망" 패턴입니다. 명령어는 잘 되는데 자동갱신이 안 돌아 만료된 경우죠.
타이머 이름은 설치 방식마다 다릅니다.
| 설치 방식 | 타이머 / 서비스 |
|---|---|
| snap (공식 권장) | snap.certbot.renew.timer / snap.certbot.renew.service |
| Debian·Ubuntu apt 패키지 | certbot.timer / certbot.service |
| RHEL 계열 EPEL 패키지 | certbot-renew.timer / certbot-renew.service |
# 설치 방식과 관계없이 certbot 관련 타이머 확인
systemctl list-timers --all | grep -i certbot
# snap 예시: 상태 확인과 최근 실행 로그
systemctl status snap.certbot.renew.timer
sudo journalctl -u snap.certbot.renew.service --since "7 days ago"
# cron 환경이라면
crontab -l; ls /etc/cron.d/ | grep -i certbot타이머가 inactive거나 목록에 없으면 자동갱신이 멈춘 겁니다. 위 표의 이름으로 다시 켭니다(snap은 sudo systemctl enable --now snap.certbot.renew.timer, apt는 certbot.timer).
cron을 직접 쓴다면 cron의 PATH에는 /snap/bin이 없을 수 있으니 절대 경로를 씁니다. snap과 apt로 certbot을 둘 다 설치하면 서로 다른 바이너리·타이머가 돌아 혼란이 생기므로 하나만 남기세요(which -a certbot으로 확인).
# crontab -e (snap 설치 기준 절대 경로)
0 3 * * * /snap/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"3단계: 재발 방지 — deploy-hook으로 nginx reload 자동화
실무에서 제가 가장 많이 본 함정은 이겁니다. 갱신은 성공했는데 브라우저는 여전히 "만료"라고 표시되는 경우. 원인은 단순합니다. nginx가 옛 인증서를 메모리에 들고 있어서 reload가 안 된 것이죠.
갱신 성공 시에만 nginx를 reload하도록 deploy-hook을 겁니다.
sudo certbot renew --deploy-hook "systemctl reload nginx"--deploy-hook을 붙여 한 번 갱신에 성공하면 certbot이 그 값을 /etc/letsencrypt/renewal/<domain>.conf의 [renewalparams] 아래 renew_hook으로 저장해 다음 갱신부터 계속 씁니다. 직접 편집한다면 반드시 [renewalparams] 섹션 안에 넣으세요.
# /etc/letsencrypt/renewal/example.com.conf
[renewalparams]
renew_hook = systemctl reload nginx모든 인증서에 공통으로 걸려면 deploy 훅 디렉터리에 실행 가능한 스크립트를 두는 방법이 가장 간단합니다.
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shACME 단기 인증서(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 --all | grep -i certbot로 자동갱신 점검(snap은snap.certbot.renew.timer)- 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를 수동 실행하고, 재발 방지를 위해 --deploy-hook이나 /etc/letsencrypt/renewal-hooks/deploy/ 스크립트로 reload를 등록하세요.
Q. 여러 도메인 중 하나만 갱신에 실패합니다.
A. 해당 도메인만 DNS A 레코드가 다른 서버를 가리키거나, 80포트 검증 경로가 막혀 있을 가능성이 큽니다. 실패한 도메인으로 dig와 curl -I http://도메인/.well-known/acme-challenge/test를 개별 확인하세요.
글마다 출처와 확인 범위가 다릅니다. 아래에 확인일·확인 범위·출처가 기록된 글만 그 범위에서 확인된 것입니다. 기록이 없는 글은 적용 전 사용 중인 버전의 공식 문서로 직접 확인하고, 오류는 이메일로 알려주세요. 편집 정책 · 이메일로 제보
이 글에는 사람이 확인한 날짜·범위 기록이 아직 없습니다.
댓글
첫 번째 댓글을 남겨보세요.