Выберите продукт

ACME renewal monitoring: systemd timer, healthcheck и правильный fullchain.pem

Разберём практичную схему мониторинга продления ACME/Let’s Encrypt: почему systemd timer удобнее cron, как правильно использовать deploy-hook для reload Nginx/Apache, зачем нужен fullchain.pem и как сделать локальные и удалённые healthcheck-проверки срока.
ACME renewal monitoring: systemd timer, healthcheck и правильный fullchain.pem

Автопродление 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 и сетевые зависимости).

Список systemd timers с отображением certbot и времени следующего запуска

Проверяем, что 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
FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

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.pemcert.pem + chain.pem (склеены).

Правило по умолчанию для веб-серверов:

  • Nginx: ssl_certificate указывает на fullchain.pem, ssl_certificate_key — на privkey.pem;
  • Apache: чаще всего SSLCertificateFilefullchain.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), либо в конфиге указан не тот путь.

Вывод openssl s_client с цепочкой сертификатов и датами окончания действия

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) любым привычным способом.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Типовые причины, почему 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, легко «залипнуть» на старом файле при ротации.

Практический чек-лист: настройка «как в проде»

  1. Убедитесь, что systemd timer для certbot включён и реально запускается. Посмотрите логи минимум за неделю.

  2. Используйте --deploy-hook для reload и делайте nginx -t или apachectl -t перед reload.

  3. В конфиге веб-сервера используйте fullchain.pem и privkey.pem из /live.

  4. Добавьте healthcheck «на диске» и healthcheck «по TLS».

  5. Периодически прогоняйте ручной тест: 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). С такой схемой вы узнаете о проблеме заранее, а не из тикета «браузер ругается на сертификат».

Поделиться статьей

Вам будет интересно

EPP-статусы домена: ok, clientHold и serverHold — что означают и как снять блокировку OpenAI Статья написана AI (GPT 5)

EPP-статусы домена: ok, clientHold и serverHold — что означают и как снять блокировку

Если домен зарегистрирован и NS прописаны, но сайт и почта не работают, часто виноваты EPP-статусы в WHOIS/RDAP. Разберём ok, clie ...
Kubernetes NodeLocal DNSCache: как победить DNS latency и NXDOMAIN storm в CoreDNS OpenAI Статья написана AI (GPT 5)

Kubernetes NodeLocal DNSCache: как победить DNS latency и NXDOMAIN storm в CoreDNS

DNS в Kubernetes часто становится скрытым узким местом: растёт latency, CoreDNS уходит в CPU, на нодах раздувается conntrack и всп ...
SSH host keys и known_hosts: ED25519 vs RSA и безопасная ротация OpenAI Статья написана AI (GPT 5)

SSH host keys и known_hosts: ED25519 vs RSA и безопасная ротация

Разбираем SSH host keys и known_hosts: где лежат ключи хоста, как клиент выбирает алгоритм, почему ssh-ed25519 чаще лучший дефолт, ...