MAATRIX / Блог / Сертификат протух в пятницу вечером: хроника и как не повторить

Сертификат протух в пятницу вечером: хроника и как не повторить

MAATRIX

Пятница, 19:40, сайт лежит — не физически, а хуже: браузер каждому посетителю показывает красный экран «Ваше подключение не защищено». Сертификат Let's Encrypt, который «сам продлевается уже два года», внезапно истёк. Дежурного нет, ответственный инженер уже за городом без ноутбука, а в логах certbot тишина — точнее, там всё написано, просто три месяца никто туда не заглядывал. Это обобщённая, но очень типичная хроника инцидента: почему автопродление ломается незаметно, как это обнаруживают (обычно не мониторингом), что делать экстренно и как сделать так, чтобы в следующий раз пятничный вечер прошёл спокойно.

Хроника: как это выглядит со стороны

Разберём типичный таймлайн, который повторяется у самых разных команд с небольшими вариациями.

  • T-90 дней. Certbot должен продлить сертификат в штатном режиме — задача в systemd-таймере или cron срабатывает, но падает с ошибкой. Ошибка уходит в лог, письмо (если вообще настроено) остаётся непрочитанным.
  • T-60 и T-30 дней. Ещё две попытки, ещё два падения. Let's Encrypt шлёт предупреждения на email, указанный при выпуске сертификата — часто это ящик уволившегося сотрудника или адрес, в который давно никто не смотрит.
  • T-0, пятница, вечер. Сертификат истекает. Nginx или Apache продолжают отдавать его как есть — TLS сам по себе не «выключает» сайт, он просто перестаёт быть валидным с точки зрения браузера.
  • T+20 минут. Пользователь пишет в поддержку: «у вас сайт взломали, ругается на безопасность». Это и есть момент обнаружения — не алерт мониторинга, а живой человек.
  • T+2 часа. Инженер диагностирует, обнаруживает, что автопродление не срабатывало уже несколько циклов, продлевает вручную, перезагружает веб-сервер.

Дальше — по пунктам: почему автопродление ломается, почему обнаруживают это не мониторингом, что делать в моменте и как обезопаситься на будущее.

Почему автопродление не сработало, хотя должно было

Let's Encrypt проектировался как раз для того, чтобы люди не сидели и не продлевали сертификаты руками — сертификат живёт 90 дней, а certbot по умолчанию пытается продлить его начиная примерно с 30-го дня до истечения, дважды в сутки через systemd-таймер certbot.timer или через cron-задачу. Проблема почти никогда не в самом Let's Encrypt — она в окружении, которое вокруг certbot незаметно поменялось.

Самые частые причины тихого сбоя:

  • Сменился путь или права на webroot. Продление через HTTP-01 с --webroot, а сайт переехал на другой каталог, сменил Docker-том или права на .well-known/acme-challenge стали 700 вместо 755 после «закручивания гаек» безопасности — certbot не может разместить файл-пруф.
  • Изменился reverse-proxy конфиг. Новый location-блок в nginx перехватывает .well-known/acme-challenge/ раньше нужного правила (общий редирект на HTTPS, SPA fallback на index.html) — challenge стабильно получает 404 или редирект вместо файла.
  • Порт 80 закрыт или занят другим сервисом. После аудита безопасности закрыли 80-й порт наружу, оставив только 443 — а HTTP-01 требует именно 80-й. При DNS-01 аналогичная история с истёкшим API-токеном DNS-провайдера.
  • Изменились права на /etc/letsencrypt. Скрипт бэкапа или смена владельца после миграции сервера поменяли права на директорию с ключами — certbot падает с PermissionError ещё до сетевого запроса.
  • Устаревшая версия certbot или плагина. На старых дистрибутивах certbot тянется из системного пакетного менеджера и может не обновляться месяцами — несовместимость с ACME-протоколом или плагином (например, nginx-плагин) встречается реже, но бывает.
  • renewal-конфиг указывает на несуществующий путь. Опечатка в webroot_path или installer внутри /etc/letsencrypt/renewal/domain.conf — и certbot откажется продлевать именно этот сертификат, при этом остальные на сервере продлятся нормально, что маскирует проблему.
  • Нет deploy-хука на перезагрузку веб-сервера. Сертификат физически продлился в /etc/letsencrypt/live/domain/, но nginx не перечитал файл, потому что не настроен --deploy-hook "systemctl reload nginx", и продолжает работать со старым.

Ключевая деталь: certbot renew завершается с ненулевым кодом выхода при ошибке, и это фиксируется в /var/log/letsencrypt/letsencrypt.log. Проверить историю попыток можно так:

grep -A5 "renew" /var/log/letsencrypt/letsencrypt.log | tail -100
sudo certbot certificates

Вывод certbot certificates сразу покажет VALID: 5 days или INVALID: EXPIRED по каждому домену — это первая команда, которую стоит запускать при любом подозрении.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как обнаружили — и почему это не мониторинг

В большинстве историй с протухшим сертификатом источник обнаружения — не система мониторинга, а жалоба пользователя или клиента. Это не случайность, а системная дыра: классический uptime-мониторинг проверяет, отвечает ли сайт на HTTP(S)-запрос кодом 200, и в подавляющем большинстве настроек не проверяет отдельно валидность и срок действия сертификата. Сайт с истёкшим сертификатом по-прежнему «отвечает» — TLS-хендшейк проходит на уровне сервера, браузер просто отказывается доверять сертификату на уровне клиента. Формально сервис жив, а по факту недоступен для всех, кто не готов кликнуть «Продолжить, зная о риске».

Отсюда практический вывод: обычный чек «сайт отвечает» и чек «сертификат ещё валиден и не истекает в ближайшие N дней» — это две разные проверки, и нужны обе. Быстро посмотреть текущий срок действия сертификата с любой машины можно так:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

Команда вернёт что-то вроде notAfter=May 1 12:00:00 2026 GMT — и если эта дата уже в прошлом или через день-два, это и есть тот самый повод не ждать пятничного вечера.

Экстренное продление: что делать в моменте

Когда обнаружили, что сертификат истёк или истекает через часы, порядок действий обычно такой:

  1. Диагностика. sudo certbot certificates и sudo certbot renew --dry-run -v — второй покажет причину сбоя без реального обращения к Let's Encrypt (важно, см. следующий раздел про лимиты).
  2. Исправление первопричины. Если дело в правах на webroot — вернуть 755 на .well-known, если в nginx-конфиге — поправить location-блок и перезагрузить конфиг перед повторной попыткой.
  3. Реальное продление. sudo certbot renew --cert-name example.com --force-renewal — форсирует выпуск нового сертификата, даже если по внутренним расчётам certbot считает, что рано.
  4. Применение сертификата. Обязательно sudo systemctl reload nginx (или apache2, или перезапуск контейнера) — иначе процесс веб-сервера продолжит держать в памяти старый файловый дескриптор истёкшего сертификата.
  5. Проверка. Повторный openssl s_client с внешней машины (не с самого сервера — там могут быть локальные DNS-хитрости), плюс проверка в браузере в приватном окне, чтобы исключить кэш HSTS/сертификатов.
sudo certbot renew --cert-name example.com --force-renewal --deploy-hook "systemctl reload nginx"

Флаг --deploy-hook здесь не лишний — если раньше его не было, самое время добавить, чтобы в будущем перезагрузка веб-сервера происходила автоматически при каждом успешном продлении.

Почему это не всегда быстро: лимиты и недоступный challenge

Важный нюанс, который удлиняет инцидент именно в пятницу вечером: Let's Encrypt — бесплатный публичный сервис с лимитами на запросы, защищающими инфраструктуру от automation-ошибок (в том числе от циклов вида «certbot падает — cron перезапускает — снова запрашивает сертификат»). Точные текущие цифры стоит смотреть в официальной документации (letsencrypt.org/docs/rate-limits/), поскольку они периодически пересматриваются, но механика такая: есть ограничение на количество выданных сертификатов на домен за скользящее окно и отдельно — на число неудачных попыток авторизации. Если сервер уже несколько раз безуспешно пытался продлить сертификат в последние дни (те самые тихие падения из первого раздела), можно упереться именно в этот лимит в момент, когда продлить нужно немедленно.

Вторая причина, по которой «просто продлить» не всегда быстро: если HTTP-01 challenge технически недоступен (закрытый порт 80, неверный webroot, конфликтующий location-блок), certbot будет упорно повторять попытку и падать — важно сначала чинить причину недоступности challenge, а не долбить renew в надежде, что само пройдёт. Диагностика через --dry-run перед реальной попыткой экономит время: она не тратит лимит и сразу показывает, пройдёт ли challenge технически.

Если авария случилась именно из-за исчерпанного лимита, единственный честный вариант — подождать, пока окно лимита сдвинется (обычно дни, не часы), либо временно выпустить сертификат другим методом (DNS-01 через другого ACME-клиента), если инфраструктура это позволяет.

Профилактика: мониторить срок действия отдельно от факта работы автопродления

Главный вывод разбора: нельзя полагаться на то, что «автопродление настроено — значит, работает». Автопродление — это процесс, который может тихо ломаться месяцами, и о его реальном здоровье нужно знать независимо от самого certbot.

1. Отдельный мониторинг именно даты истечения сертификата. Это не то же самое, что мониторинг доступности сайта. Практический вариант — Uptime Kuma умеет мониторить именно срок действия TLS-сертификата и слать алерт за N дней до истечения отдельным типом проверки, без привязки к HTTP-коду ответа. Настройка описана в статье про настройку Uptime Kuma для мониторинга сайта и сервера. Если решение самописное — тот же принцип реализуется скриптом на cron, который раз в сутки гоняет openssl s_client + openssl x509 -enddate, парсит дату и шлёт алерт при остатке меньше 14 дней:

#!/bin/bash
DOMAIN="example.com"
DAYS_LEFT=$(( ( $(date -d "$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)" +%s) - $(date +%s) ) / 86400 ))

if [ "$DAYS_LEFT" -lt 14 ]; then
  echo "Сертификат $DOMAIN истекает через $DAYS_LEFT дней" | mail -s "SSL alert" ops@example.com
fi

2. Регулярный certbot renew --dry-run как отдельная проверка, а не только при инциденте. Это ключевая рекомендация: не ждать реальной попытки продления раз в 60 дней, а гонять dry-run еженедельно через отдельную cron-задачу, логировать вывод и алертить на ненулевой код выхода:

0 6 * * 1 root certbot renew --dry-run >> /var/log/certbot-dryrun.log 2>&1 || \
  echo "certbot dry-run упал, см. /var/log/certbot-dryrun.log" | mail -s "certbot dry-run FAILED" ops@example.com

Такой прогон не тратит лимиты Let's Encrypt (dry-run использует staging-окружение) и обнаруживает поломку webroot, конфига или прав задолго до того, как это станет реальной аварией.

3. Мониторинг самого факта выполнения задачи, а не только её содержимого. Если certbot запускается через systemd-таймер, стоит убедиться, что таймер вообще активен:

systemctl status certbot.timer
systemctl list-timers | grep certbot

Для проверки именно факта «задача выполнялась» удобно завязать cron/таймер на внешний heartbeat-сервис, который бьёт тревогу, если не получил сигнал в ожидаемое окно — это отдельно решает проблему «cron вообще перестал запускаться». Подход разобран в статье про мониторинг cron-задач через healthchecks.io.

4. Алерты не только на почту, которую никто не читает. Если единственный канал уведомлений — личная почта уволившегося сотрудника, проблема не в certbot, а в процессе. Алерты о сроке действия сертификата и о падениях dry-run должны идти в общий канал команды (Telegram, Slack, PagerDuty), а не в личный ящик одного человека.

5. Чек-лист после каждой миграции. Любое изменение webroot, reverse-proxy правил, портов или прав доступа к .well-known/acme-challenge — повод сразу прогнать certbot renew --dry-run, не дожидаясь планового цикла. Это дешевле, чем найти проблему через три месяца.

Таблица ниже сводит частые причины сбоя и что именно проверять:

Причина сбояКак диагностироватьКак чинить
Права на webroot изменилисьls -la /var/www/.well-known/acme-challengeВернуть 755 на каталог
Location-блок nginx перехватывает challengecurl -I http://example.com/.well-known/acme-challenge/testПоправить порядок location-блоков
Порт 80 закрытsudo ufw status / iptables -LОткрыть 80 для HTTP-01 или перейти на DNS-01
Права на /etc/letsencryptls -la /etc/letsencryptВернуть владельца root, права по умолчанию
Нет deploy-хукаcat /etc/letsencrypt/renewal/domain.confДобавить --deploy-hook "systemctl reload nginx"
Исчерпан лимит запросовОшибка rateLimited в логе certbotПодождать окно лимита или сменить challenge-метод

Похожий разбор конкретной ошибки «сертификат не обновился сам» с более детальными сценариями и логами — в статье SSL-сертификат не обновился, а если экстренное продление у вас уже упёрлось именно в ограничение количества запросов — там же логика разобрана подробнее в материале про превышение лимита запросов Let's Encrypt.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли доверять письмам Let's Encrypt о скором истечении сертификата как единственному способу узнать о проблеме?

Нет. Эти письма — полезный дополнительный сигнал, но они уходят на email, указанный при выпуске сертификата, который может быть неактуален, попадать в спам или просто не читаться вовремя. Нужен независимый мониторинг именно даты истечения сертификата, не завязанный на чтение конкретного почтового ящика.

Certbot точно пытался продлить сертификат, просто ничего не вышло, или он вообще не запускался?

Разница принципиальная, и проверяется отдельно: systemctl status certbot.timer и journalctl -u certbot.timer покажут, запускался ли таймер в принципе, а grep renew /var/log/letsencrypt/letsencrypt.log — были ли реальные попытки и с какой ошибкой они падали.

После экстренного продления сертификат обновился, но браузер всё равно ругается — что не так?

Чаще всего веб-сервер не перечитал новый сертификат (нужен systemctl reload nginx или аналог), либо сработал локальный кэш HSTS/сертификатов в конкретном браузере — стоит проверить в приватном окне или с другого устройства, а также убедиться через openssl s_client, что сервер реально отдаёт новый сертификат, а не старый.

Стоит ли переходить на DNS-01 challenge, чтобы не зависеть от доступности порта 80?

Это разумный вариант для инфраструктур, где порт 80 закрыт по требованиям безопасности или где сайт стоит за CDN, скрывающим прямой доступ к серверу. Плата за это — зависимость от API DNS-провайдера и его токенов, которые тоже могут протухнуть или потерять права, так что это не избавляет от необходимости мониторинга, а просто переносит его на другую точку отказа.

Как часто на самом деле стоит гонять certbot renew --dry-run — раз в неделю не избыточно?

Раз в неделю — разумный баланс: dry-run ничего не стоит с точки зрения лимитов (использует staging-окружение Let's Encrypt) и достаточно часто, чтобы поймать поломку конфига в течение нескольких дней после того, как она произошла, а не через месяцы на реальном цикле продления.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →