Certbot не обновил сертификат: почему ломается автопродление и как заметить это за две недели
Certbot не обновляет сертификат? Разбор по тексту ошибки из лога: webroot, редирект, порт 80, DNS-01, лимиты, нет reload nginx — и как узнавать о сбое за 14 дней.
Certbot не обновил сертификат почти всегда по одной из двух причин: либо certbot renew вообще не запускался (таймер исчез вместе с пакетом, cron не тот), либо запускался, но валидация не прошла — и он молча писал ошибку в /var/log/letsencrypt/letsencrypt.log дважды в сутки, пока сертификат доживал последние 30 дней. Третий вариант коварнее: сертификат обновлён, файлы в /etc/letsencrypt/live/ свежие, а nginx держит в памяти старый и отдаёт его до первого reload.
Ниже — как устроено автопродление, где искать причину, типовые поломки с текстом ошибки из лога и исправлением, как убедиться, что клиенты видят новый сертификат, и как узнавать о сбое за две недели, а не от браузера с красным замком. Письма от Let's Encrypt не помогут: с июня 2025 года напоминания об истечении они не рассылают.
Как устроено автопродление
Certbot ничего не планирует сам. Дважды в сутки внешний планировщик запускает certbot renew, тот перебирает /etc/letsencrypt/renewal/*.conf и для каждого сертификата смотрит дату окончания. Если до неё больше 30 дней (по умолчанию — треть срока жизни; для 90-дневного сертификата это renew_before_expiry = 30 days), сертификат пропускается. Если меньше — certbot повторяет выпуск с теми же параметрами, что были при первом certbot certonly: тот же способ валидации, тот же webroot, тот же аккаунт. Новые версии certbot дополнительно учитывают рекомендуемое окно продления от ACME-сервера (ARI).
Сертификат Let's Encrypt живёт 90 дней, значит у автоматики есть 30 дней и около 60 попыток. Поэтому сломанное продление не замечают: одна неудача ничего не значит, а о серии сообщать некому.
Кто именно запускает certbot renew, зависит от способа установки:
| Установка | Планировщик | Как проверить |
|---|---|---|
| apt (Debian/Ubuntu) | certbot.timer → certbot.service | systemctl list-timers certbot.timer |
| snap (способ, который рекомендует Let's Encrypt) | snap.certbot.renew.timer | systemctl list-timers 'snap.certbot*' |
Пакетный /etc/cron.d/certbot в Debian проверяет, нет ли systemd, и на обычном сервере не срабатывает — работает таймер. Если apt-версию удалили и поставили pip-версию, таймер ушёл вместе с пакетом, а оставшийся /etc/cron.d/certbot молча пропускает запуск (test -x /usr/bin/certbot) — планировщика нет, certbot renew для pip-установки нужно ставить в cron или timer самому.
Renewal-конфиг /etc/letsencrypt/renewal/example.ru.conf — то, что certbot повторяет:
[renewalparams]
account = 3f2a9c...
authenticator = webroot
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa
[[webroot_map]]
example.ru = /var/www/example.ru/public
www.example.ru = /var/www/example.ru/public
Всё это было верно в день выпуска. Переехал сайт, поменялся конфиг nginx, удалили аккаунт — файл об этом не знает.
Где смотреть
certbot renew --dry-run — прогон продления через staging-сервер Let's Encrypt, без расхода лимитов и без замены файлов. Проходит — валидация рабочая; падает — текст ошибки у вас сразу, а не через месяц. Одно «но»: dry-run не выполняет deploy-хуки (для этого есть флаг --run-deploy-hooks), поэтому проблему «не перезагружен nginx» он не покажет.
$ certbot renew --dry-run
Simulating renewal of an existing certificate for example.ru and www.example.ru
Failed to renew certificate example.ru with error: Some challenges have failed.
All simulated renewals failed.
Лог /var/log/letsencrypt/letsencrypt.log ротируется при каждом запуске (letsencrypt.log.1, .2 …), так что вчерашний прогон таймера лежит в одном из старых:
grep -l "Failed to renew" /var/log/letsencrypt/letsencrypt.log* | head
grep -h -i "detail\|urn:ietf:params:acme:error" /var/log/letsencrypt/letsencrypt.log.1
Поле detail в ответе ACME-сервера (в отчёте certbot оно же Detail:) — то, что увидел валидатор Let's Encrypt со своей стороны: «404», «Connection refused», «Timeout during connect». Оно важнее всего остального.
Таймер и его последние запуски: systemctl list-timers --all | grep -i certbot и journalctl -u certbot.service --since "7 days ago". certbot certificates показывает срок по файлам в /etc/letsencrypt/live. Если там всё свежее, а браузер показывает старый срок, проблема в шаге «после продления», о нём ниже.
Типовые причины отказа
Не тот webroot
Самая частая ошибка в логе:
Detail: 203.0.113.10: Invalid response from
http://example.ru/.well-known/acme-challenge/K7hQ...: 404
Certbot положил файл-ответ в каталог из webroot_map, а nginx отдаёт /.well-known/ из другого места: сайт переехал в /var/www/example.ru/current, фреймворк перехватывает всё через try_files $uri /index.php, у www и без www разные root. Надёжнее не привязывать проверку к каталогу сайта — в каждом server { listen 80; }, выше остальных location:
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
}
и переписать renewal-конфиг одной командой, не редактируя файл руками:
certbot certonly --webroot -w /var/www/letsencrypt -d example.ru -d www.example.ru --force-renewal
Проверка без certbot: положите файл t в /var/www/letsencrypt/.well-known/acme-challenge/ и запросите его: curl -sS -o /dev/null -w '%{http_code}\n' http://example.ru/.well-known/acme-challenge/t — должно быть 200, а не 301 и не 404; без -o /dev/null в ответе должно прийти содержимое файла, а не HTML.
Редирект на HTTPS и порядок директив nginx
Валидатор Let's Encrypt ходит по редиректам, в том числе на https://, и не проверяет при этом сертификат — на сайте с истёкшим сертификатом http-01 тоже проходит. Ломает не редирект, а то, куда он ведёт. Классика:
server {
listen 80;
return 301 https://$host$request_uri;
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
}
return на уровне server выполняется на фазе server rewrite — до выбора location, поэтому блок для .well-known здесь не сработает никогда. Запрос уходит на 443, там такого location нет, и приложение отвечает 404. Исправление — перенести редирект внутрь location /:
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
location / { return 301 https://$host$request_uri; }
Либо продублировать location для .well-known в блоке listen 443 ssl — тогда редирект не помеха. Вариант той же болезни — редирект на https://www. ведёт в server-блок другого сайта или на CDN.
Порт 80 закрыт или слушает не тот адрес
Detail: 203.0.113.10: Fetching http://example.ru/.well-known/acme-challenge/K7hQ...:
Timeout during connect (likely firewall problem)
Порт 80 закрыли «за ненадобностью» после переезда на HTTPS — в ufw, в файрволе панели провайдера, в security group. Для http-01 порт 80 должен быть открыт для всего интернета: Let's Encrypt валидирует с нескольких точек и не публикует их адреса. Connection refused вместо таймаута обычно значит, что nginx не слушает 80 (ss -ltnp | grep ':80 '). Если в строке Detail: стоит IPv6-адрес, дело в AAAA-записи: валидатор идёт по ней первой — нужен listen [::]:80 или удаление AAAA.
С authenticator standalone симптом обратный: Problem binding to port 80 — порт занят веб-сервером. Либо перейти на webroot, либо останавливать nginx хуками --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx" ценой короткого простоя.
DNS-01 без хука
Wildcard *.example.ru выпускается только через dns-01, и первый раз его часто получают вручную: certbot certonly --manual --preferred-challenges dns, добавили TXT-запись, готово. Через 60 дней в логе:
Failed to renew certificate example.ru with error: The manual plugin is not working;
there may be problems with your existing configuration. The error was:
PluginError('An authentication script must be provided with --manual-auth-hook
when using the manual plugin non-interactively.')
Некому создать новую TXT-запись. Варианты: плагин под DNS-провайдера (certbot-dns-cloudflare, certbot-dns-rfc2136 для своего BIND) или скрипты --manual-auth-hook / --manual-cleanup-hook, которые ходят в API регистратора. Проверяйте, что токен API живой: токены отзываются при смене пароля или истекают, и в выводе хука будет ошибка авторизации от API провайдера (обычно 401 или 403). Если wildcard не нужен, проще выпустить обычный сертификат на список поддоменов через http-01.
Лимиты Let's Encrypt
urn:ietf:params:acme:error:rateLimited :: too many certificates (5) already issued
for this exact set of identifiers in the last 168h0m0s
Лимиты, о которые бьются при отладке: 5 одинаковых сертификатов (тот же набор имён) в неделю, 5 неудачных валидаций на имя в час, 50 сертификатов на зарегистрированный домен в неделю. Точные значения — в документации Let's Encrypt. Типичный сценарий: админ пять раз подряд запускает certbot --force-renewal, чинит конфиг — и упирается в недельный лимит на дубликаты. Отлаживайте через --dry-run (staging, отдельные лимиты) и только рабочий прогон делайте на боевом endpoint. Если лимит исчерпан, добавьте в сертификат ещё одно имя — набор идентификаторов станет другим.
Аккаунт, старый endpoint, снапшоты
Сервер перенесли, /etc/letsencrypt скопировали без accounts/, или restore из бэкапа вернул live/ и archive/, но не accounts/ и renewal/. В логе — No such file or directory: '/etc/letsencrypt/accounts/...' или urn:ietf:params:acme:error:accountDoesNotExist. Лечится certbot register и перевыпуском через certonly. На старых серверах в renewal-конфиге встречается server = https://acme-v01.api.letsencrypt.org/directory — endpoint выключен в 2021 году; замените на acme-v02 и обновите certbot.
Откат на снапшот двухмесячной давности возвращает и старый /etc/letsencrypt: уже обновлённый сертификат снова старый, certbot продлит его штатно, если порт 80 открыт. С клонами хуже: образ сняли, DNS переключили на новый сервер, старый оставили — у него валидация падает, потому что домен указывает на другой IP. После любого restore первая команда — certbot certificates.
Сертификат обновлён, а сайт отдаёт старый
Самый обидный вариант: certbot certificates показывает 80 дней запаса, а браузер — истёкший сертификат. Certbot переписал symlink в live/example.ru/ на новые файлы в archive/, но nginx читает сертификат один раз при старте или reload. Плагины --nginx и --apache перезагружают сервер сами; webroot и dns-* — нет, если вы не попросили.
Способ попросить — deploy-хук, который выполняется только когда сертификат реально обновлён. Файл /etc/letsencrypt/renewal-hooks/deploy/reload.sh с правом на выполнение:
#!/bin/sh
nginx -t && systemctl reload nginx
Каталог renewal-hooks/deploy/ certbot обходит сам, флагов не нужно. Не путайте с post-hook: он выполняется и при неудачной попытке продления (поэтому в нём принято запускать nginx после --pre-hook), но, как и deploy, не срабатывает, когда ни один сертификат не подошёл к окну продления. В хуке доступны $RENEWED_LINEAGE и $RENEWED_DOMAINS — удобно, когда один сертификат раздаётся нескольким службам. Nginx — не единственный, кто кэширует сертификат:
| Служба | Что сделать в хуке |
|---|---|
| Postfix, Dovecot, Exim | systemctl reload postfix dovecot |
| HAProxy | склеить fullchain.pem + privkey.pem в один файл, затем reload |
| Docker-контейнер | docker exec app nginx -s reload; монтировать весь /etc/letsencrypt, а не live/ — там symlink на ../../archive/ |
| Панель хостинга | хранит копию сертификата у себя: файл на диске обновится, а панель продолжит раздавать свой |
Какой сертификат реально видит клиент
Смотреть надо снаружи, а не в файлах:
echo | openssl s_client -connect example.ru:443 -servername example.ru 2>/dev/null \
| openssl x509 -noout -enddate -issuer -serial
notAfter=Nov 12 09:14:33 2026 GMT
issuer=C=US, O=Let's Encrypt, CN=R13
serial=04A1F3...
Тот же -serial для файла: openssl x509 -in /etc/letsencrypt/live/example.ru/cert.pem -noout -serial. Серийные номера разошлись — сервер не перезагружен. Ещё три проверки, о которых забывают: IPv6 (-connect '[2001:db8::1]:443'), другие порты (-connect mail.example.ru:465 — почтовый сертификат продлевает тот же certbot, а reload postfix в хук не добавили) и обход CDN (-connect 203.0.113.10:443 -servername example.ru, где 203.0.113.10 — IP origin: посетителям CDN отдаёт свой сертификат, а истёкший на origin ломает соединение CDN с сервером — у Cloudflare в режиме Full (strict) это ошибка 526, а в режиме Full он молча принимается, и истечение остаётся незамеченным). Без терминала то же покажет онлайн-проверка SSL-сертификата с нескольких точек.
openssl x509 -checkend 1209600 возвращает код 1, если сертификат истекает в ближайшие 14 дней (1 209 600 секунд) — на этом строится простейший сторож.
Как узнать о проблеме за две недели
Ключевое число — 14 дней. Штатное продление происходит за 30 дней до конца, и если спустя 16 дней сертификат всё ещё старый, автоматика сломана: 30 неудачных попыток подряд — не случайность. Двух недель хватает, чтобы починить webroot, дождаться сброса лимита или написать DNS-хук.
Минимальный вариант — скрипт в /etc/cron.daily/ на самом сервере с сообщением в Telegram:
#!/bin/sh
echo | openssl s_client -connect example.ru:443 -servername example.ru 2>/dev/null \
| openssl x509 -noout -checkend 1209600 >/dev/null \
|| curl -s "https://api.telegram.org/bot$TOKEN/sendMessage" \
-d chat_id="$CHAT" -d text="SSL example.ru истекает меньше чем через 14 дней"
У скрипта два слепых пятна: он молчит, когда лёг сам сервер или отозван Telegram-токен, и проверяет с того же хоста — ошибки CDN, IPv6 и неполной цепочки он не увидит. Внешний мониторинг SSL-сертификата закрывает оба: проверяет с точек в России и Европе раз в сутки, предупреждает за 14 дней до истечения и отдельно сообщает, что сертификат сменился — так видно и удачное продление, и подмену; сообщение приходит в Telegram, на почту или по SMS.
Второе, что стоит контролировать, — сам факт запуска certbot renew: если таймер исчез, проверка сертификата сработает через 60 дней после поломки. Хуки certbot для этого не годятся: в прогонах, где продлевать нечего (а таких примерно 58 из 60), они не выполняются. Факт запуска снимайте с самой службы: systemctl edit certbot.service (для snap — snap.certbot.renew.service) и добавьте в drop-in строку, которая сработает после каждого прогона, включая холостые:
[Service]
ExecStartPost=/usr/bin/logger -t certbot-renew ok
Дальше либо просматривайте journalctl -t certbot-renew, либо вместо logger поставьте curl на URL heartbeat-проверки, которая поднимает тревогу, когда отметка от задания перестаёт приходить.
Коротко
certbot renewзапускается дважды в сутки таймером или cron, продлевает только сертификаты со сроком меньше 30 дней и складывает ошибки в/var/log/letsencrypt/без уведомлений.- Первые три команды:
certbot renew --dry-run,grep -i detail /var/log/letsencrypt/letsencrypt.log*,systemctl list-timers | grep certbot. - Большинство отказов — валидация http-01: 404 из-за webroot,
return 301на уровнеserver, закрытый порт 80, AAAA-запись в никуда,--manualбез хука для dns-01. - «Сертификат обновлён, а сайт отдаёт старый» — нет reload; положите скрипт в
/etc/letsencrypt/renewal-hooks/deploy/, не забудьте почту, HAProxy и контейнеры. - Проверяйте снаружи:
openssl s_client ... | openssl x509 -noout -enddate -serial; серийный номер должен совпадать с файлом вlive/. - Не отлаживайте на боевом endpoint — лимит 5 дубликатов в неделю; за две недели до истечения должен сработать сторож, не браузер.
Вопросы и ответы
Как заставить certbot продлить сертификат прямо сейчас?
Почему certbot пишет «not due for renewal» и ничего не делает?
Сертификат уже истёк — certbot всё ещё сможет его продлить?
Certbot установлен через snap — где его таймер и логи?
Как проверить, что deploy-хук вообще выполняется?
Нужно ли открывать порт 80, если сайт работает только по HTTPS?
Не проверяйте вручную — следите автоматически
Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.
Подключить бесплатно Проверить сайт сейчас