
certbot renew 실패 에러 해결 — Let's Encrypt 자동 갱신 안 될 때 원인 6가지
certbot renew 실패 에러는 대부분 80포트 점유·webroot 경로·플러그인 충돌·rate limit·systemd 타이머 미작동 중 하나다. Let's Encrypt 자동 갱신이 안 될 때 먼저 할 진단부터 원인별 해결 명령, 재발 방지까지 실무 기준으로 정리했다.
새벽에 모니터링 봇이 울렸다. "SSL 인증서 만료 D-3". 분명 작년에 certbot을 걸어두고 자동 갱신까지 확인했는데, 왜 하필 지금 멈춰 있을까. 사이드 프로젝트나 개인 서버를 돌려본 사람이라면 한 번쯤 겪는 장면이다. Let's Encrypt 인증서는 수명이 90일뿐이라, 자동 갱신이 조용히 고장 나 있어도 두세 달은 멀쩡해 보인다. 그러다 만료 임박 메일이 날아오고 나서야 certbot renew가 빨간 글씨를 뱉는 걸 발견한다.
이 글은 그 빨간 글씨 앞에서 뭘 먼저 봐야 하는지, 그리고 실제로 갱신을 막는 원인 여섯 가지를 하나씩 끊어서 정리한 것이다. 명령은 전부 복사해서 바로 쓸 수 있게 적었다.

손대기 전에, 지금 상태부터 찍어본다
원인을 추측하기 전에 사실부터 확인하는 게 빠르다. 두 명령이면 충분하다.
sudo certbot certificates
sudo certbot renew --dry-run
certbot certificates는 이 서버가 들고 있는 인증서, 포함된 도메인, 만료일, 그리고 어떤 방식(webroot/nginx/standalone)으로 발급됐는지를 한눈에 보여준다. 만료일이 코앞인데 "VALID"로 떠 있다면 발급 자체는 됐고 갱신 스케줄이 안 돈 것이다.
renew --dry-run은 Let's Encrypt의 스테이징 서버를 상대로 실제 발급 없이 갱신 과정만 흉내 내는 리허설이다. 운영 한도(rate limit)를 깎지 않으니 마음껏 돌려도 된다. 여기서 에러가 재현되면 로그 마지막 줄을 그대로 읽는다. 대부분의 범인은 이 한 줄에 다 들어 있다.
sudo tail -n 40 /var/log/letsencrypt/letsencrypt.log
원인 1 — 80포트가 막혔거나 다른 프로세스가 물고 있다
Let's Encrypt의 HTTP-01 검증은 80포트로 들어온다. 이 길이 막히면 "Timeout during connect" 또는 "Connection refused"가 뜬다. 먼저 누가 80을 쓰는지 본다.
sudo ss -tlnp | grep ':80'
sudo ufw status
방화벽에서 80이 닫혀 있으면 열어준다. 클라우드라면 보안그룹(인바운드 규칙)도 같이 봐야 한다 — 서버 안에서 열어도 클라우드 콘솔에서 막혀 있으면 소용없다.
sudo ufw allow 80/tcp
국내 가정용 회선이라면 통신사가 80포트 자체를 차단해 둔 경우가 많다. 이건 서버 설정으로 못 뚫는다. DNS-01 방식으로 갈아타거나 외부에서 접근 가능한 회선을 써야 한다.
원인 2 — webroot 경로가 실제 문서 루트와 어긋났다
webroot 방식은 certbot이 지정한 폴더/.well-known/acme-challenge/ 아래에 인증 파일을 떨어뜨리고, 웹서버가 그걸 그대로 서빙해야 성사된다. 발급 당시 경로와 지금 nginx 루트가 달라졌다면 "unauthorized" 또는 404가 난다. 저장된 경로부터 확인한다.
sudo cat /etc/letsencrypt/renewal/example.com.conf
webroot_path 값이 현재 서버 블록의 root와 같은지 본다. 어긋났다면 올바른 경로로 한 번 재발급해 설정을 덮어쓴다.
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
저는 여기서 두 번 당했다. nginx 설정을 갈아엎으면서 root를 옮겼는데 certbot 쪽 webroot_path는 옛날 경로 그대로였다. 갱신은 3개월 뒤에 조용히 깨진다. 설정을 바꿨으면 그날 바로 --dry-run을 돌려보는 습관이 유일한 예방책이다.
원인 3 — standalone과 웹서버가 80을 두고 싸운다
standalone 플러그인은 certbot이 잠깐 자체 웹서버를 띄워 80포트를 쓰는 방식이다. nginx가 이미 80을 점유 중이면 충돌한다. 발급 땐 nginx를 잠깐 껐다가 켰는데, 자동 갱신 때는 그 과정이 없어서 실패하는 전형적인 사례다.

해결은 두 갈래다. 웹서버를 끄지 않아도 되는 방식으로 바꾸거나(권장), standalone을 유지하되 갱신 전후로 서버를 멈췄다 켜는 훅을 건다.
sudo certbot renew --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
다만 이 방식은 갱신 순간 몇 초간 서비스가 끊긴다. 무중단이 중요하면 nginx 플러그인이나 webroot로 옮기는 편이 낫다. nginx 플러그인은 포트를 뺏지 않고 설정을 임시로 손봐서 검증한다.
sudo certbot renew --nginx
원인 4 — Let's Encrypt rate limit에 걸렸다
같은 설정을 고치겠다고 발급 명령을 수십 번 반복하면 한도에 걸린다. Let's Encrypt는 등록 도메인당 7일에 50장, 완전히 같은 도메인 조합(중복 인증서)은 7일에 5장까지만 내준다. 검증 실패도 계정·도메인당 1시간에 5번이 상한이다. "too many certificates" 또는 "too many failed authorizations"가 바로 이것이다.
한도에 걸렸다면 답은 하나, 기다리는 것이다. 그 사이 설정 테스트는 반드시 --dry-run이나 --staging으로만 돌린다. 운영 한도를 더 깎지 않는다.
sudo certbot renew --dry-run
원인 5 — 자동 갱신 타이머가 애초에 안 돌고 있었다
certbot renew를 손으로 치면 되는데 저절로는 안 된다면, 스케줄러가 죽은 것이다. 설치 방식에 따라 이름이 다르다. apt로 깔았으면 certbot.timer, snap으로 깔았으면 snap.certbot.renew.timer다.
systemctl list-timers | grep -i certbot
여기에 아무것도 안 뜨면 타이머가 비활성이거나 아예 없는 것이다. apt 설치 기준으로 살려본다.
sudo systemctl enable --now certbot.timer
systemctl status certbot.timer
cron으로 돌리던 구형 설정이면 /etc/cron.d/certbot을 확인한다. snap과 apt를 한 서버에 둘 다 깔아놔서 타이머가 꼬인 경우도 흔하니, 하나로 정리하는 게 깔끔하다.
원인 6 — 도메인이나 DNS가 바뀌었는데 설정은 그대로다
도메인을 옮겼거나 A레코드를 바꿨는데 인증서에 박힌 이름은 옛날 그대로면 검증이 깨진다. 지금 도메인이 이 서버를 가리키는지부터 본다.
dig +short example.com
이 IP가 현재 서버 공인 IP와 다르면 DNS부터 맞춰야 한다. 반대로 도메인을 하나 뺐거나 추가했다면 -d 목록을 새로 지정해 재발급해야 한다.
자동 갱신이 진짜 도는지 확인
고쳤다고 끝이 아니다. 타이머가 언제 다음에 깨어나는지, 지난번엔 제대로 돌았는지를 눈으로 본다.
systemctl list-timers | grep -i certbot
sudo journalctl -u certbot.service -n 50 --no-pager
그리고 갱신이 끝나면 웹서버가 새 인증서를 다시 읽도록 리로드하는 훅이 걸려 있어야 한다. 이게 빠지면 인증서는 갱신됐는데 브라우저엔 여전히 옛 인증서가 나가는, 가장 허무한 사고가 난다.
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
echo -e '#!/bin/sh\nsystemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
재발 방지 체크
- 설정(특히 nginx root·포트)을 건드린 날엔 그 자리에서
certbot renew --dry-run한 번. - 만료 30일 전 알림을 따로 건다. certbot은 만료 직전에야 갱신을 시도하므로, 그 전에 깨진 걸 알 장치가 없으면 90일마다 가슴을 쓸어내린다.
- deploy 훅에 서버 리로드를 반드시 넣는다.
- 설치 방식(apt/snap)을 서버마다 통일해 타이머 꼬임을 막는다.
여기까지가 명령 모음이고, 조금 더 현실적인 이야기를 덧붙이고 싶다. 국내에서 개인 서버를 돌리는 사람들이 유독 많이 걸리는 함정이 둘 있다. 하나는 방금 말한 가정용 회선 80포트 차단이다. KT·SKB 같은 통신사 가정용 상품은 80·443을 막아두는 경우가 있어서, 클라우드에선 되던 HTTP-01 검증이 집 서버에선 통째로 안 된다. 이럴 땐 플러그인을 바꾸는 게 아니라 아예 DNS-01(도메인 TXT 레코드로 검증)로 전략을 틀어야 한다 — Cloudflare를 쓴다면 certbot-dns-cloudflare 플러그인이 거의 표준처럼 쓰인다. 다른 하나는 Docker로 nginx를 띄운 경우다. 호스트에 깔린 certbot이 컨테이너 안 nginx의 문서 루트를 못 봐서 webroot 검증이 깨지는데, 이건 경로 문제지 certbot 버그가 아니다. 볼륨으로 .well-known 경로를 공유하거나, nginx 컨테이너 안에서 certbot을 함께 돌리는 구성을 써야 한다. 결국 certbot 트러블슈팅의 8할은 "90일 전 발급 때와 지금 환경이 뭐가 달라졌나"를 찾는 일이다. 자동 갱신은 걸어두는 걸로 끝이 아니라, 갱신 후 리로드와 타이머 생존까지가 한 세트라는 걸 잊지 않으면 새벽에 알림 받을 일이 확 줄어든다.
인증서 갱신이 막히는 원인은 결국 포트·경로·플러그인·한도·타이머·도메인 여섯 갈래로 수렴한다. 추측하지 말고 certbot certificates와 --dry-run으로 사실부터 찍은 뒤, 로그 마지막 줄이 가리키는 쪽만 손대면 대부분 10분 안에 끝난다. 여러분은 이 중 어떤 걸로 가장 많이 당해봤는지, 혹시 여기 없는 희한한 케이스가 있었는지 댓글로 공유해 주면 다음에 겪는 사람에게 큰 도움이 된다.
참고
댓글 0
댓글을 불러오는 중…