Коротко. Автопродление Let's Encrypt выполняет systemd-таймер certbot.timer или cron-задание: certbot дважды в сутки обходит сертификаты и обновляет те, которым осталось меньше тридцати дней. Если продление встало, начните с certbot renew --dry-run — он воспроизводит настоящую проверку и не расходует боевые лимиты. Дальше смотрите конфиг в /etc/letsencrypt/renewal/, доступность /.well-known/acme-challenge/ снаружи и наличие deploy-hook с перезагрузкой веб-сервера.
Эта статья — про отказы, а не про первый выпуск. Как поставить certbot и получить сертификат с нуля, разобрано отдельно: бесплатный SSL от Let's Encrypt: установка и настройка. Здесь мы исходим из того, что сертификат когда-то выпустился и работал, а потом перестал обновляться, — и разбираем каждый типовой сценарий отказа с командой проверки и фиксом.
Как устроено автопродление certbot и когда оно срабатывает
Автопродление — это не магия внутри certbot, а внешний планировщик, который регулярно вызывает одну команду: certbot renew. Дальше certbot обходит все файлы в каталоге /etc/letsencrypt/renewal/, для каждого сертификата смотрит остаток срока и обновляет только те, которым осталось меньше порога. По умолчанию порог — примерно тридцать дней до истечения.
Планировщиков в природе три, и на одном сервере может оказаться сразу два:
- systemd-таймер
certbot.timer— вариант пакетов Debian/Ubuntu и snap-установки. Запускается дважды в сутки со случайной задержкой, чтобы весь интернет не ломился к CA ровно в полночь. - cron-задание
/etc/cron.d/certbot— старая схема, тоже дважды в сутки, обычно со случайнымsleepв начале. - собственный скрипт или контейнерный цикл — типично для Docker: entrypoint, который в бесконечном цикле спит и вызывает renew.
Проверка, что планировщик вообще существует и работает:
# systemd-таймер: существует ли, когда сработает, когда срабатывал
systemctl list-timers --all | grep -i certbot
systemctl status certbot.timer
systemctl cat certbot.service
# cron-вариант
cat /etc/cron.d/certbot 2>/dev/null
# что за сертификаты есть и когда истекают
certbot certificates
# журнал последних запусков
journalctl -u certbot --since "30 days ago" --no-pager | tail -n 60
tail -n 200 /var/log/letsencrypt/letsencrypt.log
Ключевой момент, который обычно упускают: окно продления открывается сильно заранее. Certbot начинает пытаться примерно за месяц до истечения и повторяет попытки дважды в сутки. То есть у вас есть около шестидесяти неудачных попыток и целый месяц запаса, прежде чем посетители увидят ошибку в браузере. Обратная сторона — отказ не заметен: успешное продление ничего не пишет наружу, а неуспешное ложится в лог и там остаётся.
Если вы узнали о проблеме от браузера или от клиента, значит месяц молчаливых ошибок уже прошёл. Правильная точка обнаружения — не истёкший сертификат, а первый неуспешный certbot renew --dry-run.

certbot renew --dry-run: главная команда диагностики
certbot renew --dry-run проделывает полный цикл продления против тестового окружения ACME: строит запрос, проходит проверку владения доменом, получает тестовый сертификат — и ничего не записывает в /etc/letsencrypt/live/. Это единственный безопасный способ узнать, продлится ли сертификат, до того как это станет срочным.
# проверить все сертификаты на сервере
certbot renew --dry-run
# только один сертификат (быстрее и понятнее в выводе)
certbot renew --cert-name example.com --dry-run
# подробности, если ошибка невнятная
certbot renew --cert-name example.com --dry-run -v
Успешный вывод выглядит примерно так:
Processing /etc/letsencrypt/renewal/example.com.conf
Simulating renewal of an existing certificate for example.com and www.example.com
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)
Отказ — так:
Failed to renew certificate example.com with error: Some challenges have failed.
All simulated renewals failed. The following certificates could not be renewed:
/etc/letsencrypt/live/example.com/fullchain.pem (failure)
Дальше нужен не сам факт отказа, а причина. Она лежит на строку-две выше в выводе и всегда целиком — в /var/log/letsencrypt/letsencrypt.log. Именно текст ошибки (Timeout, 404, NXDOMAIN, unauthorized) определяет, какой из разделов ниже вам нужен.
Важное ограничение: --dry-run по умолчанию не выполняет deploy-hook. Успешный dry-run доказывает, что сертификат получится выпустить, но не доказывает, что веб-сервер подхватит новый файл. В свежих версиях certbot есть отдельный флаг, заставляющий выполнить deploy-хуки в режиме dry-run; если его нет — проверяйте перезагрузку сервиса вручную.
Практическое правило: гоняйте --dry-run не когда что-то сломалось, а по расписанию — раз в месяц и обязательно после любого изменения конфигурации nginx, DNS, файрвола, WAF или переезда сайта в другой каталог. Полторы секунды проверки экономят ночной инцидент.
Конфиг продления: /etc/letsencrypt/renewal/<домен>.conf
Для каждого сертификата certbot хранит файл с параметрами, с которыми его выпустили. При продлении используются именно они, а не то, что вы набирали в консоли в прошлый раз. Если сайт переехал, сменил веб-сервер или способ проверки — этот файл устарел, и продление будет падать до тех пор, пока его не привести в соответствие.
# cat /etc/letsencrypt/renewal/example.com.conf
archive_dir = /etc/letsencrypt/archive/example.com
cert = /etc/letsencrypt/live/example.com/cert.pem
privkey = /etc/letsencrypt/live/example.com/privkey.pem
chain = /etc/letsencrypt/live/example.com/chain.pem
fullchain = /etc/letsencrypt/live/example.com/fullchain.pem
[renewalparams]
account = 4a1f...
authenticator = webroot
webroot_path = /var/www/example.com/public,
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa
[[webroot_map]]
example.com = /var/www/example.com/public
www.example.com = /var/www/example.com/public
Что здесь важно читать:
authenticator— способ подтверждения владения:webroot,nginx,apache,standaloneили DNS-плагин. Половина отказов — это authenticator, который больше не соответствует реальности (например,standaloneна сервере, где теперь постоянно работает nginx).webroot_pathи блок[[webroot_map]]— куда certbot кладёт файл-ответ. Должно совпадать с тем каталогом, из которого веб-сервер раздаёт статику для этого домена.installer— кто правит конфиг веб-сервера после выпуска. Если плагин удалён или веб-сервер сменился, продление падает на этапе установки, хотя сертификат уже получен.server— адрес ACME-эндпоинта. Если здесь тестовый (staging) URL, продление формально успешно, но сертификат недоверенный: браузеры ругаются на неизвестный центр сертификации.renew_before_expiry— окно продления, если оно задано явно.key_type— тип ключа. Смена типа требует перевыпуска, а не правки файла.
Править этот файл руками — крайняя мера и источник трудноуловимых ошибок. Штатный способ изменить параметры продления — повторить выпуск с тем же --cert-name: certbot перезапишет конфиг корректно и целиком.
# переключить сертификат на webroot и новый каталог, не меняя имя сертификата
certbot certonly --cert-name example.com \
--webroot -w /var/www/example.com/public \
-d example.com -d www.example.com \
--dry-run
# убедились, что сработало, — повторяем без --dry-run
certbot certonly --cert-name example.com \
--webroot -w /var/www/example.com/public \
-d example.com -d www.example.com
HTTP-01 не проходит: редиректы, webroot, порт 80, WAF
Проверка HTTP-01 устроена просто: certbot кладёт файл в /.well-known/acme-challenge/<токен>, а серверы удостоверяющего центра запрашивают его по обычному HTTP на порт 80 и сверяют содержимое. Ломается это пятью способами.
1. Редирект на HTTPS съедает путь запроса
Сам по себе редирект http → https не запрещён: валидатор следует за перенаправлениями. Смертельны две конкретные формулировки. Первая — редирект, который теряет URI:
# ПЛОХО: путь /.well-known/acme-challenge/... превращается в /
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host/;
}
Вторая — редирект на HTTPS-хост, где нужный каталог не раздаётся: другой root, другой server-блок, приложение-фронтконтроллер, которое на неизвестный путь отдаёт свою страницу 404. Итог тот же — валидатор получает не тот ответ.
Надёжное решение — явное исключение для ACME до общего редиректа. Префикс ^~ в nginx гарантирует, что этот location выиграет у регулярных выражений:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# исключение для ACME — обязательно ВЫШЕ общего редиректа
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com/public;
default_type "text/plain";
auth_basic off;
allow all;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
Отдельная ловушка: return 301, написанный на уровне server, а не внутри location /, перебивает все location-блоки. Если исключение для ACME не срабатывает — первым делом ищите такой return. Подробнее про приоритет location и типовые ошибки — в руководстве по конфигурации nginx, а про корректный перевод сайта на HTTPS — в статье про миграцию с HTTP на HTTPS.
Проверка, которая занимает двадцать секунд и отвечает на вопрос окончательно:
mkdir -p /var/www/example.com/public/.well-known/acme-challenge
echo ok > /var/www/example.com/public/.well-known/acme-challenge/test
# смотрим всю цепочку редиректов и финальный код
curl -sSIL http://example.com/.well-known/acme-challenge/test | grep -E 'HTTP/|[Ll]ocation'
# смотрим тело ответа — должно быть ровно "ok"
curl -sSL http://example.com/.well-known/acme-challenge/test
rm /var/www/example.com/public/.well-known/acme-challenge/test
2. Ответ 404: webroot указывает не туда
Классика после переезда или смены фреймворка: webroot_path остался /var/www/html, а сайт давно раздаётся из /var/www/example.com/public. Certbot успешно пишет файл — в каталог, из которого никто ничего не раздаёт. Симптом в логе: Invalid response ... 404. Фикс — перевыпуск с правильным -w, как показано выше.
3. Порт 80 закрыт или занят
HTTP-01 всегда стучится на 80-й порт: переназначить его на стороне CA нельзя. Закрытый файрволом, security group облака или самим провайдером 80-й порт даёт в логе Timeout during connect (likely firewall problem).
# слушает ли кто-то 80 локально
ss -lntp | grep ':80 '
# правила файрвола
nft list ruleset 2>/dev/null | grep -n 'dport 80' || iptables -S | grep -- '--dport 80'
# доступность снаружи (с другой машины)
curl -sS -o /dev/null -w '%{http_code}\n' http://example.com/
Локальный ss показывает только половину картины: сервер может слушать порт, а трафик не доходить. Проверяйте снаружи — например, через сканер портов.
4. Basic-auth, WAF и защита от ботов
Если сайт целиком закрыт базовой авторизацией, валидатор получает 401 и проверка падает. То же самое делают WAF и антибот-защиты: они отдают JS-челлендж, капчу или 403 на запрос без cookies и с непривычным User-Agent. Валидатор ACME — это не браузер: он не выполняет JavaScript, не хранит cookies и не проходит проверки.
Лечение — точечное исключение пути /.well-known/acme-challenge/ из-под авторизации и из-под правил WAF (в nginx это auth_basic off; внутри соответствующего location, как в конфиге выше). Если сайт стоит за внешним прокси или CDN с включённым проксированием и защитой, запрос до вашего сервера может просто не дойти — тогда надёжнее перейти на проверку через DNS-01.
5. Блокировка по стране, ASN или списку IP
Отдельная и очень частая причина в рунете. Удостоверяющий центр проверяет домен не из одной точки, а из нескольких сетевых позиций в разных регионах мира, и все они должны получить корректный ответ. Если вы закрыли сайт от зарубежного трафика — фильтром по стране, по ASN или белым списком подсетей, — HTTP-01 перестаёт проходить, хотя из России сайт открывается идеально.
Гео-блокировка и автопродление Let's Encrypt несовместимы, если только вы не оставили открытым путь /.well-known/acme-challenge/ для всего мира. Альтернатива — перейти на DNS-01, где сетевая доступность сайта вообще не проверяется.

DNS, SAN и переезды: домен не резолвится, поддомен умер, сервер сменился
Домен перестал резолвиться или A-запись переехала
Ошибка вида DNS problem: NXDOMAIN looking up A for example.com означает, что валидатор не нашёл адрес домена. Причины: домен не продлён, зона не делегирована, запись удалили при переносе DNS, NS-серверы сменились и старые уже не отвечают.
# резолвится ли имя и куда указывает
dig +short A example.com
dig +short AAAA example.com
# кто сейчас авторитетный сервер зоны
dig +short NS example.com
# не запрещает ли CAA выпуск для Let's Encrypt
dig +short CAA example.com
CAA-запись — недооценённая причина. Если в зоне есть CAA, разрешающая выпуск только одному центру сертификации, а Let's Encrypt в списке нет, продление падает с ошибкой политики, хотя сайт полностью доступен. Такое случается, когда админ добавил CAA под другого поставщика сертификатов и забыл про certbot. Свериться с фактическим состоянием зоны удобно через проверку DNS-записей, а если запись только что правили — через проверку распространения DNS.
Один мёртвый поддомен валит весь сертификат
Сертификат Let's Encrypt может содержать несколько имён в поле SAN. При продлении проверяются все имена, и достаточно одного, которое больше не резолвится или недоступно, чтобы продление всего сертификата упало. Типичный сценарий: в сертификате остался old.example.com или staging.example.com, проект давно закрыт, запись удалена — и вместе с ней ломается основной домен.
# посмотреть, какие имена входят в сертификат
certbot certificates
# то же самое из файла
openssl x509 -noout -text -in /etc/letsencrypt/live/example.com/fullchain.pem \
| grep -A1 'Subject Alternative Name'
# перевыпуск без мёртвого имени: перечисляем ПОЛНЫЙ новый список -d
certbot certonly --cert-name example.com \
--webroot -w /var/www/example.com/public \
-d example.com -d www.example.com \
--dry-run
Обратите внимание: список -d задаётся целиком, а не «вычитанием». Что перечислили — то и будет в сертификате. Ненужные сертификаты целиком удаляются командой certbot delete --cert-name old.example.com — иначе они будут вечно висеть в очереди продления и вечно падать.
Переезд сервера: /etc/letsencrypt не перенесли
Сертификат и ключ скопировали, а каталог /etc/letsencrypt — нет. На новом сервере нет ни ACME-аккаунта, ни renewal-конфигов, ни архива: продлевать нечего, и никакой таймер не сработает. Иногда бывает хуже — конфиги скопировали обычным cp, симлинки в live/ превратились в файлы, и certbot перестаёт понимать структуру каталога.
# перенос с сохранением симлинков, прав и владельцев
rsync -aAX --numeric-ids /etc/letsencrypt/ root@new-host:/etc/letsencrypt/
# на новом сервере — обязательная проверка
certbot certificates
certbot renew --dry-run
Приватные ключи в/etc/letsencrypt/archive/— секрет уровня пароля от сервера. Переносите их только по защищённому каналу, сохраняйте права0600и владельцаroot, не кладите в общие бэкапы без шифрования и не копируйте через промежуточные машины.
Альтернатива переносу — просто выпустить сертификат на новом сервере заново до переключения DNS, используя DNS-01. Это чище: старый сервер продолжает работать со своим сертификатом, новый получает свой, переключение трафика не создаёт окна без TLS.
DNS-01: протухший ключ провайдера, старый плагин, слишком короткое ожидание
Проверка DNS-01 не требует доступности сайта: certbot создаёт TXT-запись _acme-challenge.example.com через API DNS-провайдера, ждёт распространения и просит валидатор её прочитать. Это единственный способ получить wildcard-сертификат и лучший вариант для сайтов за CDN, WAF или гео-фильтром. Свои три способа сломаться у него тоже есть.
- Ключ API протух или изменился. Провайдеры регулярно меняют модель доступа: глобальный ключ заменяют на токен с ограниченной областью, старые ключи отзывают. Плагин в ответ получает 401 или 403, а certbot пишет что-то про ошибку плагина, а не про домен. Проверяется правкой файла с учётными данными и повторным
--dry-run. - Плагин устарел относительно API. Особенно если certbot ставился системным пакетом, а плагин — из другого источника. Симптом: плагин работал год, никто ничего не менял, и вдруг перестал.
- Слишком короткое ожидание распространения. Certbot по умолчанию ждёт фиксированное время и идёт проверять. Если у провайдера зона расходится по серверам дольше, валидатор читает старое состояние. Лечится увеличением параметра
--dns-<провайдер>-propagation-seconds(например,--dns-cloudflare-propagation-seconds 60).
# права на файл с ключами обязаны быть строгими
chmod 600 /etc/letsencrypt/dns-credentials.ini
ls -l /etc/letsencrypt/dns-credentials.ini
# видна ли TXT-запись публичным резолверам и авторитетному серверу
dig +short TXT _acme-challenge.example.com
dig +short TXT _acme-challenge.example.com @1.1.1.1
dig +short TXT _acme-challenge.example.com @ns1.example.com
# прогон с увеличенным ожиданием
certbot renew --cert-name example.com \
--dns-cloudflare-propagation-seconds 60 --dry-run
Устойчивый приём для сложных случаев: делегировать _acme-challenge.example.com через CNAME в отдельную зону на DNS-хостинге с нормальным API. Тогда основную зону трогать не нужно, а certbot управляет только маленькой служебной зоной — меньше прав, меньше риска.
Standalone: порт 80 занят nginx
Режим standalone поднимает собственный временный веб-сервер на 80-м порту. Если сертификат когда-то выпускали до установки nginx, в renewal-конфиге навсегда остался authenticator = standalone, и при продлении certbot получает:
Problem binding to port 80: Could not bind to IPv4 or IPv6.
Три варианта решения, по убыванию правильности:
- Перейти на webroot. Правильный вариант для сервера, где постоянно работает веб-сервер: никакого простоя, никаких хуков. Делается перевыпуском с
--cert-nameи--webroot -w. - Оставить standalone на нестандартном порту и проксировать. Certbot слушает локальный порт, а nginx проксирует на него только путь ACME. Работает, но конструкция сложнее, чем webroot, без выигрыша.
- Останавливать и запускать веб-сервер хуками. Годится, когда веб-сервера нет вовсе (почтовый сервер, VPN, брокер), но за сертификатом всё равно нужен порт 80.
# разово, в команде
certbot renew --cert-name mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"
# постоянно, для всех сертификатов — скрипты в каталогах хуков
/etc/letsencrypt/renewal-hooks/pre/
/etc/letsencrypt/renewal-hooks/post/
/etc/letsencrypt/renewal-hooks/deploy/
Скрипты из этих каталогов certbot выполняет автоматически при каждом renew: pre — до попытки, post — после (независимо от результата), deploy — только если сертификат действительно обновился. Не забудьте chmod +x: неисполняемый файл молча игнорируется.
Rate limits Let's Encrypt: почему «попробую ещё раз» делает хуже
У Let's Encrypt есть ограничения на количество операций за период: и на выпуск сертификатов для одного набора имён, и — отдельно — на количество неуспешных проверок владения доменом. Конкретные значения менялись, актуальные всегда смотрите в документации Let's Encrypt по лимитам.
Практический вывод один и он важный: повторные попытки на боевом окружении не чинят проблему, а закрывают вам дверь. Каждый неуспешный certbot renew расходует лимит неудачных проверок. Упершись в него, вы получаете отказ на несколько часов — уже независимо от того, починили вы причину или нет. Сайт при этом остаётся без сертификата.
Порядок ровно такой: сначала--dry-run(тестовое окружение, отдельные лимиты), находим и чиним причину, повторяем--dry-runдо успеха, и только потом один боевойcertbot renew. Не наоборот.
Отдельно про --force-renewal: этот флаг игнорирует окно продления и выпускает сертификат заново, даже если до истечения ещё месяцы. В диагностике он почти никогда не нужен, зато отлично сжигает лимит выпуска для набора имён. Используйте его осознанно — например, при смене типа ключа, — а не как «кнопку на всякий случай».
Если лимит уже исчерпан, а сертификат нужен прямо сейчас, есть законный обходной путь: лимит считается для конкретного набора имён. Сертификат на суженный список (только example.com без www) — это другой набор, со своим счётчиком. Как временная мера годится.
Сертификат обновился, а браузер отдаёт старый: забыли deploy-hook
Самая обидная категория: certbot отработал успешно, файлы в /etc/letsencrypt/live/ свежие, а сайт продолжает отдавать просроченный сертификат. Причина в том, что nginx, Apache, Postfix, Dovecot и HAProxy читают сертификат при старте и держат его в памяти. Новый файл на диске сам по себе им безразличен.
Диагностика — сравнить дату в файле и дату, которую отдаёт живой сервис:
# что лежит на диске
openssl x509 -noout -dates -subject \
-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 -dates -subject -issuer
Даты разошлись — значит нужен deploy-hook. Он выполняется только после фактического обновления сертификата, поэтому его безопасно вешать на перезагрузку сервиса:
# разово в команде
certbot renew --deploy-hook "systemctl reload nginx"
# постоянно и для всех сертификатов
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/sh
set -e
nginx -t
systemctl reload nginx
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Нюансы по сервисам:
- nginx, Apache — достаточно
reload. Обязательно с предварительнымnginx -t/apachectl configtest: сломанный конфиг при reload оставит вас со старым процессом и без понимания, почему ничего не изменилось. - Postfix, Dovecot —
reloadобычно достаточно, но некоторые сборки подхватывают ключи только при полном рестарте. Проверяйте так же, черезopenssl s_client, но по портам 25, 465, 587 и 993 с флагом-starttls smtpтам, где нужно. - HAProxy — ждёт один файл, где склеены цепочка и приватный ключ. Deploy-hook должен сначала собрать этот файл из
fullchain.pemиprivkey.pem, и только потом перезагружать сервис. - Контейнеры — hook обязан перезагрузить процесс в контейнере, а не на хосте.

Два certbot, два таймера: snap, apt и pip на одном сервере
Certbot ставится минимум тремя способами, и каждый приносит свой планировщик. Когда на сервере оказываются два, начинается интересное: вручную запущенный certbot работает, а по расписанию продление падает; или два таймера обращаются к одному каталогу разными версиями; или cron-задание вызывает /usr/bin/certbot, а вы всё это время обновляли snap-версию в /snap/bin/certbot.
# сколько вообще certbot в системе и какой выигрывает по PATH
which -a certbot
certbot --version
# откуда каждый
snap list certbot 2>/dev/null
dpkg -l 2>/dev/null | grep -i certbot
rpm -qa 2>/dev/null | grep -i certbot
pip3 show certbot 2>/dev/null
# сколько планировщиков
systemctl list-timers --all | grep -iE 'certbot|snap\.certbot'
ls -l /etc/cron.d/ | grep -i certbot
Правило простое: один источник установки, один планировщик. Лишний пакет удаляется вместе со своим таймером или cron-файлом. Отдельно проверьте, что cron-задание вызывает certbot по абсолютному пути — окружение cron беднее интерактивного, и certbot: command not found в логе крона означает ровно это.
Ещё одна деталь: системные пакеты certbot в LTS-дистрибутивах заметно отстают от актуальной версии. Старый certbot может не поддерживать нужный DNS-плагин или новый формат ответов ACME. Если продление начало падать без изменений с вашей стороны — проверьте версию.
Certbot в Docker: тома, время, перезагрузка соседнего контейнера
Контейнерная схема добавляет собственный набор граблей, и все они про изоляцию.
Контейнер не видит webroot
Самая частая ошибка: nginx раздаёт /usr/share/nginx/html, а certbot пишет в /var/www/certbot, и эти каталоги смонтированы из разных мест или вообще не пересекаются. Файл-ответ создаётся, но веб-сервер про него не знает.
# видит ли nginx то, что записал certbot
docker compose exec certbot sh -c 'echo ok > /var/www/certbot/.well-known/acme-challenge/test'
docker compose exec nginx ls -la /var/www/certbot/.well-known/acme-challenge/
# и снаружи
curl -sSL http://example.com/.well-known/acme-challenge/test
# полный прогон
docker compose run --rm certbot renew --dry-run
Анонимный том вместо именованного
Если /etc/letsencrypt смонтирован анонимным томом, пересоздание контейнера теряет и ACME-аккаунт, и renewal-конфиги, и архив ключей. Внешне это выглядит как «certbot почему-то ничего не продлевает»: продлевать нечего, каталог пустой. Том обязан быть именованным или bind-монтом с хоста.
Время в контейнере уехало
ACME чувствителен к часам: расхождение даёт ошибки вида badNonce или жалобы на срок действия. Контейнер обычно берёт время с ядра хоста, поэтому чинить нужно хост.
date -u
docker compose exec nginx date -u
timedatectl status
timedatectl set-ntp true
Перезагрузка nginx из контейнера certbot
Deploy-hook внутри контейнера certbot не видит процесс nginx в соседнем контейнере. Рабочие варианты: sidecar-скрипт, который периодически выполняет nginx -s reload внутри своего контейнера; или перезапуск контейнера nginx по расписанию раз в сутки; или общий том с файлом-флагом, который nginx-контейнер отслеживает.
Не монтируйте /var/run/docker.sock в контейнер certbot ради удобной перезагрузки соседа. Доступ к сокету Docker эквивалентен root на хосте: любая уязвимость в контейнере превращается в полную компрометацию сервера. Ради reload nginx это несоразмерный риск.
Почему письма от Let's Encrypt — не система оповещения
Let's Encrypt может присылать предупреждения о скором истечении на адрес, указанный при регистрации ACME-аккаунта. На этот механизм нельзя опираться по трём причинам: адрес часто принадлежит уволившемуся сотруднику или подрядчику; письма уходят в спам; сам объём таких уведомлений удостоверяющий центр со временем сокращает.
# посмотреть и обновить контактный адрес аккаунта
certbot show_account
certbot update_account --email ops@example.com
Актуальный адрес поставить стоит — это бесплатно и иногда спасает. Но единственная надёжная страховка — внешний мониторинг срока действия, который смотрит на живой сайт, а не на файл на диске, и оповещает заранее: за тридцать, за четырнадцать и за семь дней. Тогда сломанное автопродление обнаруживается в первые сутки, а не в момент, когда сертификат уже истёк. Как это устроить — в статье про мониторинг срока действия SSL-сертификата; готовый инструмент — мониторинг доступности и SSL.
Проверка снаружи: почему файл на диске врёт
Файл в /etc/letsencrypt/live/ отвечает на вопрос «что получил certbot», но не на вопрос «что видит посетитель». Между ними — минимум четыре места, где картинка расходится: невыполненный reload, несколько server-блоков с разными сертификатами, SNI и default_server, а также балансировщик или CDN, который терминирует TLS у себя и держит собственную копию.
# что отдаётся по каждому IP домена — важно при нескольких A-записях
for ip in $(dig +short A example.com); do
echo "== $ip"
echo | openssl s_client -connect "$ip:443" -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
done
# сколько дней осталось (быстрый ответ да/нет)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 604800 && echo "больше 7 дней" || echo "меньше 7 дней"
# полная цепочка, как её видит клиент
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null \
| grep -E 's:|i:'
Флаг -servername обязателен: без него сервер отдаст сертификат default_server, и вы будете чинить не тот сертификат. Отдельно проверьте полноту цепочки — обновлённый сертификат с недосланным промежуточным звеном ломает часть клиентов, и это выглядит как «сертификат не обновился». Разбор — в статье про неполную цепочку сертификатов; практические способы посмотреть сертификат — в материале как проверить SSL-сертификат сайта.
Шпаргалка: симптом → причина → проверка → фикс
| Симптом или сообщение | Вероятная причина | Команда проверки | Фикс |
|---|---|---|---|
Invalid response ... 404 |
Неверный webroot или редирект теряет путь | curl -sSIL http://d/.well-known/acme-challenge/test |
Перевыпуск с верным -w; location ^~ для ACME выше редиректа |
Timeout during connect (likely firewall problem) |
Порт 80 закрыт файрволом, security group или провайдером | ss -lntp | grep ':80 ', проверка порта снаружи |
Открыть 80 везде по пути; проверить nft/iptables и облачные правила |
DNS problem: NXDOMAIN looking up A |
Имя не резолвится: удалена запись, сменились NS, домен не продлён | dig +short A d, dig +short NS d |
Вернуть A-запись или убрать мёртвое имя из сертификата |
| Отказ по политике CAA | CAA-запись разрешает выпуск другому центру сертификации | dig +short CAA d |
Добавить letsencrypt.org в CAA или удалить лишнюю запись |
unauthorized, ответ 401 или 403 |
Basic-auth, WAF, антибот или гео-фильтр перед сайтом | curl -sSI http://d/.well-known/acme-challenge/test |
auth_basic off; и исключение пути в WAF, либо переход на DNS-01 |
| Падает только один из доменов, но сертификат не продлевается целиком | Мёртвое имя в SAN | certbot certificates |
Перевыпуск с полным новым списком -d, без мёртвого имени |
Problem binding to port 80: Could not bind |
authenticator = standalone, порт занят nginx |
ss -lntp | grep ':80 ' |
Перейти на webroot; либо pre/post-hook со стопом веб-сервера |
| Слишком много неуспешных проверок или выпусков | Упёрлись в лимит после серии повторов | tail -n 200 /var/log/letsencrypt/letsencrypt.log |
Пауза, диагностика только через --dry-run, потом один боевой renew |
| Плагин DNS отдаёт 401 или 403 | Ключ API отозван, сменилась модель токенов, устарел плагин | certbot renew --cert-name d --dry-run -v |
Новый токен в credentials-файл, chmod 600, обновить плагин |
| TXT-запись не видна валидатору | Слишком короткое ожидание распространения зоны | dig +short TXT _acme-challenge.d |
Увеличить --dns-<провайдер>-propagation-seconds |
| Файл свежий, браузер отдаёт старый сертификат | Нет deploy-hook: сервис держит старый сертификат в памяти | openssl s_client против файла на диске |
--deploy-hook "systemctl reload nginx" или скрипт в renewal-hooks/deploy |
| Вручную renew работает, по расписанию — нет | Два certbot из разных источников, два таймера, разный PATH | which -a certbot, systemctl list-timers --all |
Оставить один источник и один планировщик, вызывать по абсолютному пути |
В Docker: No such file or directory на webroot |
Тома certbot и nginx не пересекаются | docker compose exec nginx ls -la /var/www/certbot/... |
Смонтировать один именованный том в оба контейнера |
badNonce или жалобы на срок действия |
Съехали часы на хосте или в контейнере | timedatectl status, date -u |
timedatectl set-ntp true, синхронизация времени на хосте |
| Браузер: «недоверенный центр сертификации» после успешного продления | В renewal-конфиге прописан тестовый ACME-эндпоинт | grep server /etc/letsencrypt/renewal/d.conf |
Перевыпуск на боевом эндпоинте, без флага тестового окружения |
The following certs are not due for renewal |
Не отказ: окно продления ещё не открылось | certbot certificates |
Ничего не делать; проверить работоспособность через --dry-run |

Что делать прямо сейчас, если сертификат уже истёк
Порядок действий, когда времени на теорию нет:
- Подтвердите факт снаружи, а не по файлу:
openssl s_clientс-servernameили онлайн-проверка SSL. Иногда «истёк» — это на самом деле неполная цепочка или не тот server-блок. - Не запускайте renew в цикле. Каждая неудачная попытка приближает вас к лимиту, после которого выпуск будет запрещён на часы.
- Проверьте базовое: открыт ли 80-й порт, резолвится ли домен, отдаёт ли
/.well-known/acme-challenge/тестовый файл. - Получите настоящую ошибку:
certbot renew --cert-name example.com --dry-run -vи полный текст из/var/log/letsencrypt/letsencrypt.log. - Почините причину по таблице выше и повторяйте
--dry-run, пока он не пройдёт. - Один боевой запуск:
certbot renew --cert-name example.com. Истёкший сертификат всегда попадает в окно продления,--force-renewalне нужен. - Перезагрузите сервис и убедитесь, что снаружи отдаётся новый сертификат.
- Если упёрлись в лимит — как временная мера годится сертификат на суженный набор имён: у другого набора свой счётчик.
- Поставьте мониторинг в тот же день, пока помните, чем это кончилось.
Более подробный разбор аварийного сценария, включая случаи не от certbot, — в статье про то, что делать, если SSL-сертификат истёк.
Как проверить
Диагностику удобно вести снаружи — оттуда же, откуда домен видит удостоверяющий центр:
- Проверка SSL-сертификата — фактические даты выпуска и истечения, полнота цепочки, соответствие домену. Это первый шаг: он отвечает, действительно ли проблема в продлении.
- Диагностика ошибок SSL — разбор конкретного сообщения браузера: недоверенный центр, несовпадение имени, истёкший срок, оборванная цепочка.
- Проверка цепочки редиректов — покажет, куда на самом деле уходит запрос к
/.well-known/acme-challenge/и не теряется ли по дороге путь. Самая частая причина отказа HTTP-01 видна именно здесь. - Мониторинг сайта и SSL — постоянный контроль срока действия с оповещением заранее. Обязательная страховка от повторения истории.
- Сканер портов — открыт ли 80-й порт снаружи, а не только локально.
- Проверка DNS-записей — A, NS и CAA домена глазами внешнего резолвера.
Частые вопросы
Certbot пишет «not due for renewal» — это ошибка?
Нет, это нормальное поведение: до истечения ещё больше порогового срока, продлевать нечего. Проверить работоспособность механизма в этот момент можно только через certbot renew --dry-run — он выполняет реальную проверку домена независимо от остатка срока.
Насколько часто должен запускаться таймер?
Стандартная схема — дважды в сутки со случайной задержкой. Это даёт около шестидесяти попыток за месячное окно продления и распределяет нагрузку на удостоверяющий центр. Чаще запускать бессмысленно, реже — опасно: одна пропущенная попытка перестаёт компенсироваться остальными.
Меняется ли приватный ключ при продлении?
По умолчанию certbot генерирует новый ключ при каждом продлении. Это правильное поведение с точки зрения безопасности. Если инфраструктура завязана на конкретный ключ (например, включён HPKP-подобный пиннинг или ключ прошит в клиенте), есть флаг --reuse-key, но применять его стоит осознанно: долгоживущий ключ — долгоживущий риск.
Что делать, если 80-й порт недоступен в принципе?
Переходить на проверку DNS-01: она не требует ни доступности сайта, ни открытых портов, только API-доступ к DNS-зоне. Это же единственный способ получить wildcard-сертификат и рабочий вариант для сайтов за CDN, WAF или гео-фильтром.
Нужно ли перевыпускать сертификат при смене IP-адреса сервера?
Нет: сертификат выпускается на доменное имя, а не на IP. Но продление проверяет домен, поэтому A-запись должна указывать на сервер, который умеет отвечать на ACME-проверку. При переезде переносите /etc/letsencrypt целиком и сразу прогоняйте --dry-run на новом месте.
Помогает ли --force-renewal, когда продление падает?
Нет. Флаг только игнорирует окно продления, а причину отказа — недоступный challenge, мёртвый домен, сломанный конфиг — не устраняет. Зато каждый запуск расходует лимит выпуска. При отказах используйте --dry-run, а --force-renewal оставьте для осознанных задач вроде смены типа ключа.
Чеклист: продление, которое не ломается
certbot renew --dry-runпроходит успешно прямо сейчас — проверено, а не предположено.--dry-runстоит в расписании раз в месяц и запускается после каждой правки nginx, DNS, файрвола или WAF.- Планировщик один: либо systemd-таймер, либо cron, либо контейнерный цикл. Проверено через
systemctl list-timersиwhich -a certbot. - В
/etc/letsencrypt/renewal/нет конфигов от давно закрытых доменов. - Список имён в сертификате соответствует реальности: мёртвых поддоменов нет.
- В конфиге nginx есть location
^~ /.well-known/acme-challenge/выше общего редиректа, сauth_basic off;. - Порт 80 открыт снаружи, путь ACME не закрыт гео-фильтром, WAF и антиботом.
- Настроен deploy-hook с перезагрузкой всех сервисов, использующих сертификат: веб, почта, прокси.
- Deploy-hook исполняемый (
chmod +x) и содержит проверку конфига перед reload. - Каталог
/etc/letsencryptпопадает в бэкап и переносится целиком при миграции, с сохранением симлинков и прав. - В renewal-конфиге прописан боевой ACME-эндпоинт, а не тестовый.
- Контактный адрес ACME-аккаунта актуален, но процесс на письма не завязан.
- Внешний мониторинг срока действия оповещает минимум за тридцать дней и смотрит на живой сайт, а не на файл.
- Есть записанный порядок действий на случай «сертификат истёк» — чтобы в аварии не импровизировать.