Автопродление ACME-сертификатов обычно «работает, пока работает», а потом внезапно выясняется, что сертификат просрочен: таймер не запускался, DNS временно глючил, или веб-сервер так и не перечитал новые файлы. Ниже — рабочая схема «как в проде»: certbot через systemd timer (вместо ручного cron), мониторинг срока действия (локальный и по TLS) и корректная работа с fullchain.pem, плюс безопасный --deploy-hook для reload Nginx/Apache.
Что именно нужно контролировать в ACME-автопродлении
В цепочке ACME-обновления есть три критичных шага, и мониторинг должен закрывать их все:
- проверка владения доменом (HTTP-01/DNS-01/TLS-ALPN-01);
- получение нового сертификата и запись на диск (обновление симлинков в
/etc/letsencrypt/live/...); - перечитывание сертификата сервисами (иначе они продолжат отдавать старый из памяти).
На практике чаще всего ломается либо проверка домена, либо «сертификат обновился на диске, но фронт не перечитал». Поэтому держим в голове два вопроса:
- «сертификат на диске действительно не истекает в ближайшие дни?»
- «внешним клиентам по TLS отдаётся именно новый сертификат и полная цепочка?»
cron vs systemd timer: почему лучше systemd
Добавить certbot renew в cron легко, но в проде важнее наблюдаемость и предсказуемость. На Debian/Ubuntu certbot из пакетов часто уже ставится с certbot.service и certbot.timer. У systemd timers есть реальные плюсы:
- статус, время следующего запуска и история — через
systemctl; - логи в journald — через
journalctl; - «догоняющий» запуск после простоя — через
Persistent=true; - проще описывать зависимости (например, что сеть уже поднята).
Смотрите на renew как на прод-процесс: должен быть понятный запуск, понятные логи и проверяемый результат.
Если у вас проекты живут на нескольких узлах или вы держите инфраструктуру на облачных серверах, удобнее иметь единый подход и повторяемую схему на каждой машине (особенно на VDS, где вы полностью контролируете systemd и сетевые зависимости).

Проверяем, что certbot timer действительно существует и запускается
Начните с инвентаризации. На системах с пакетным certbot обычно есть certbot.timer и certbot.service. Если certbot установлен через snap, имена и путь могут отличаться — принцип проверки тот же.
systemctl list-timers --all | grep -i certbot
systemctl status certbot.timer
systemctl cat certbot.service
journalctl -u certbot.service --no-pager -n 200
В выводе таймера важны три вещи: включён ли он, когда был прошлый запуск и когда будет следующий. Дальше — смотрим логи последнего запуска: там будет видно, доходил ли процесс до попыток проверки домена и были ли реальные обновления.
Пример собственного systemd timer для renew (если штатного нет)
Если вы хотите полностью контролировать расписание и хуки, заведите отдельный юнит. Сервис:
cat > /etc/systemd/system/acme-renew.service <<'EOF'
[Unit]
Description=ACME renew (certbot)
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
EOF
Таймер (ежедневно, с рандомизацией и «догонялкой»):
cat > /etc/systemd/system/acme-renew.timer <<'EOF'
[Unit]
Description=Run ACME renew daily
[Timer]
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now acme-renew.timer
systemctl status acme-renew.timer
Hook’и certbot: почему для reload нужен deploy-hook
Одна из самых частых ошибок: ставят reload в --post-hook и получают «иногда перезагружается без обновления» или наоборот «обновилось, но не перечитал». Причина простая: --post-hook может выполняться даже когда обновления не было.
У certbot есть три типа хуков:
--pre-hook— перед попыткой обновления (может выполниться даже если обновление не требуется);--post-hook— после попытки (тоже может выполниться без реального обновления);--deploy-hook— выполняется только если сертификат действительно обновился.
Для «reload nginx/apache после обновления» почти всегда нужен именно --deploy-hook.
Сценарий: безопасный reload Nginx или Apache после обновления
Сделаем единый скрипт: проверит конфиг и выполнит graceful reload. Идея простая — лучше не перезагружаться вообще, чем «уронить» фронт из-за синтаксической ошибки.
cat > /usr/local/sbin/acme-deploy-reload-web <<'EOF'
#!/bin/sh
set -eu
if systemctl is-active --quiet nginx; then
nginx -t
systemctl reload nginx
exit 0
fi
if systemctl is-active --quiet apache2; then
apachectl -t
systemctl reload apache2
exit 0
fi
exit 0
EOF
chmod 0755 /usr/local/sbin/acme-deploy-reload-web
Подключение как deploy-hook:
certbot renew --deploy-hook /usr/local/sbin/acme-deploy-reload-web
Если у вас есть отдельные политики для разных сертификатов, удобнее прописать deploy-hook в renewal-конфиге конкретного сертификата. Если инфраструктура управляется systemd drop-in’ами, hook можно добавить и на уровне unit’а, но следите, чтобы это не затронуло «чужие» сертификаты.
fullchain.pem: что это и почему его постоянно путают
В каталоге /etc/letsencrypt/live/ваш-домен/ обычно лежат симлинки:
privkey.pem— приватный ключ;cert.pem— leaf-сертификат домена;chain.pem— промежуточная цепочка;fullchain.pem—cert.pem+chain.pem(склеены).
Правило по умолчанию для веб-серверов:
- Nginx:
ssl_certificateуказывает наfullchain.pem,ssl_certificate_key— наprivkey.pem; - Apache: чаще всего
SSLCertificateFile—fullchain.pem, ключ —privkey.pem(точные директивы зависят от сборки и ваших include’ов).
Типовая ошибка — указать cert.pem вместо fullchain.pem. Часть браузеров «вытянет» цепочку сама (AIA fetching), а часть клиентов (встроенные устройства, старые агенты, некоторые библиотеки) сломаются с ошибкой неполной цепочки.
Мини-проверка: что сервер реально отдаёт по TLS
Эта команда покажет, какой сертификат отдаётся прямо сейчас (полезно после обновления и reload):
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Если на диске файлы уже новые, а снаружи даты старые — проблема почти всегда в том, что reload не сработал, трафик идёт на другой узел (LB/CDN), либо в конфиге указан не тот путь.

ACME renewal monitoring: локальный и удалённый healthcheck
Надёжный подход — иметь два чека: один проверяет срок действия файла на диске (не зависит от сети и фронта), второй — проверяет то, что реально видит клиент по TLS (ловит «не перечитал» и проблемы балансировки).
Локальный healthcheck: проверяем fullchain.pem на диске
Скрипт ниже возвращает OK, если до истечения больше порога, WARN — если меньше, и CRIT — если сертификат уже истёк или файл не читается.
cat > /usr/local/sbin/acme-cert-expiry-check <<'EOF'
#!/bin/sh
set -eu
CERT_FILE="$1"
DAYS_WARN="${2:-14}"
if [ ! -r "$CERT_FILE" ]; then
echo "CRIT: cannot read $CERT_FILE"
exit 2
fi
enddate=$(openssl x509 -enddate -noout -in "$CERT_FILE" | sed 's/notAfter=//')
end_ts=$(date -d "$enddate" +%s)
now_ts=$(date +%s)
left_sec=$((end_ts - now_ts))
left_days=$((left_sec / 86400))
if [ "$left_days" -lt 0 ]; then
echo "CRIT: certificate expired ($left_days days)"
exit 2
fi
if [ "$left_days" -lt "$DAYS_WARN" ]; then
echo "WARN: certificate expires in $left_days days"
exit 1
fi
echo "OK: certificate expires in $left_days days"
exit 0
EOF
chmod 0755 /usr/local/sbin/acme-cert-expiry-check
Пример запуска:
/usr/local/sbin/acme-cert-expiry-check /etc/letsencrypt/live/example.com/fullchain.pem 21
Удалённый healthcheck: проверяем сертификат, который отдаёт сервер
Этот чек ходит по TLS и проверяет срок действия «снаружи». Он полезен, если сертификаты обновляются на одном узле, а трафик идёт на другой, или если забыли reload.
cat > /usr/local/sbin/acme-remote-expiry-check <<'EOF'
#!/bin/sh
set -eu
HOST="$1"
PORT="${2:-443}"
SNI="${3:-$HOST}"
DAYS_WARN="${4:-14}"
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
echo | openssl s_client -connect "$HOST:$PORT" -servername "$SNI" 2>/dev/null | openssl x509 -noout -enddate > "$tmp"
enddate=$(sed 's/notAfter=//' "$tmp")
end_ts=$(date -d "$enddate" +%s)
now_ts=$(date +%s)
left_days=$(((end_ts - now_ts) / 86400))
if [ "$left_days" -lt 0 ]; then
echo "CRIT: remote cert expired ($left_days days)"
exit 2
fi
if [ "$left_days" -lt "$DAYS_WARN" ]; then
echo "WARN: remote cert expires in $left_days days"
exit 1
fi
echo "OK: remote cert expires in $left_days days"
exit 0
EOF
chmod 0755 /usr/local/sbin/acme-remote-expiry-check
Запуск:
/usr/local/sbin/acme-remote-expiry-check example.com 443 example.com 21
Связываем мониторинг с systemd: отдельный timer для healthcheck
Лучше всего запускать healthcheck отдельным таймером, независимо от renew. Тогда даже если renew по какой-то причине «не запускается вообще», чек всё равно заранее поднимет сигнал.
Сервис:
cat > /etc/systemd/system/acme-cert-healthcheck.service <<'EOF'
[Unit]
Description=ACME certificate expiry healthcheck
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/acme-cert-expiry-check /etc/letsencrypt/live/example.com/fullchain.pem 21
EOF
Таймер:
cat > /etc/systemd/system/acme-cert-healthcheck.timer <<'EOF'
[Unit]
Description=Run ACME certificate healthcheck daily
[Timer]
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now acme-cert-healthcheck.timer
Дальше вы можете забирать статус/логи из journald и поднимать алерты по exit code (0/1/2) любым привычным способом.
Типовые причины, почему renew «не работает»
1) Таймер выключен или сервис падает молча
systemctl is-enabled certbot.timer
systemctl status certbot.timer
journalctl -u certbot.service --since "7 days ago" --no-pager
Если видите ошибки DNS/сети — убедитесь, что ваш unit ждёт поднятия сети (network-online.target) и что на сервере корректно настроен резолвинг.
2) HTTP-01 не проходит из-за редиректов, CDN или firewall
Для HTTP-01 внешний мир должен доступаться до /.well-known/acme-challenge/ по 80 порту. Редирект на HTTPS часто допустим, но ломается на сложных цепочках, нестандартных правилах и при «умных» CDN.
Если вам нужен wildcard или домены сидят за балансировщиками и многими фронтами, часто практичнее автоматизировать DNS-01. См. статью про автоматизацию wildcard через DNS-01: Wildcard-сертификаты и DNS-01: автоматизация без ручных TXT-записей.
3) Сертификат обновился, но Nginx/Apache продолжает отдавать старый
- использовали
--post-hookвместо--deploy-hook; - reload выполнялся, но конфиг невалиден, и сервис отказался перечитываться;
- трафик идёт не на этот хост (второй узел, LB, CDN);
- в конфиге указан
cert.pemвместоfullchain.pem.
4) Путаются пути из /live и /archive
Ссылайтесь на файлы в /etc/letsencrypt/live/...: симлинки там обновляются атомарно. Если прописать путь прямо в /etc/letsencrypt/archive, легко «залипнуть» на старом файле при ротации.
Практический чек-лист: настройка «как в проде»
Убедитесь, что systemd timer для certbot включён и реально запускается. Посмотрите логи минимум за неделю.
Используйте
--deploy-hookдля reload и делайтеnginx -tилиapachectl -tперед reload.В конфиге веб-сервера используйте
fullchain.pemиprivkey.pemиз/live.Добавьте healthcheck «на диске» и healthcheck «по TLS».
Периодически прогоняйте ручной тест:
certbot renew --dry-runи проверку выдачи черезopenssl s_client.
Отладка: команды, которые экономят часы
Посмотреть, какие сертификаты/пути реально прописаны в конфиге (быстрый sanity-check):
nginx -T 2>/dev/null | grep -n "ssl_certificate" || true
apachectl -S 2>/dev/null | head
Посмотреть, что certbot считает актуальным на хосте:
certbot certificates
Понять, когда таймер реально отрабатывал:
systemctl list-timers --all | grep -E "certbot|acme"
journalctl -u certbot.service --no-pager -n 100
Если вы упёрлись в ограничения CA (частые перевыпуски, SAN-наборы, миграции), держите под рукой разбор лимитов и типовых сценариев: SAN и rate limits Let’s Encrypt: как не упереться в ограничения при автоматизации.
Итог
Надёжная автоматизация ACME — это не только «поставить certbot». В проде важно: запуск через systemd timer (наблюдаемо и управляемо), корректный выбор fullchain.pem, правильный --deploy-hook для reload и независимый мониторинг срока действия (локальный и удалённый по TLS). С такой схемой вы узнаете о проблеме заранее, а не из тикета «браузер ругается на сертификат».


