certbot как официальный клиент Let's Encrypt: systemd-timer вместо cron, 60+ доменов за месяц
EFF выпускает certbot и официально убирает letsencrypt-auto. Мигрируем на certbot, переписываем пролонгацию на systemd-timer и закрываем 60+ клиентских доменов без единого ручного действия.
EFF выпускает certbot как официальный клиент Let's Encrypt, заменяя letsencrypt-auto; старое имя объявлено deprecated
EFF на прошлой неделе официально переименовала letsencrypt-auto в certbot и объявила его основным клиентом Let's Encrypt. Новый пакет - в EPEL, в backports Debian, в официальной документации. Старое имя помечено deprecated. Для нас это не просто rename: хороший повод пересмотреть то, что мы уже раскатали в апреле, и заодно закрыть оставшуюся очередь клиентских доменов.
К этому моменту мы прошли через GA в апреле и в первый же день закрыли 15 доменов. За апрель добавили ещё, но список ещё не исчерпан - примерно 30-40 позиций висели «на потом» из-за нестандартных конфигураций или просто потому что очередь. Официальный релиз certbot - хороший стимул дожать.
Что изменилось с новым именем
Формально - только имя пакета и бинаря. Но попутно EFF подтянул пакетную инфраструктуру: теперь certbot устанавливается из EPEL на CentOS 7 одной командой без pip, без virtualenv, без лишних зависимостей. Раньше letsencrypt-auto при первом запуске на несколько минут уходил в self-bootstrap - тащил Python-зависимости через pip. Это было неудобно при первоначальной установке через Ansible: задача с непредсказуемым временем выполнения, зависящая от pip-зеркала. Теперь просто yum install certbot.
Наша Ansible-роль из марта использовала вызов через /usr/local/bin/letsencrypt с путём из virtualenv. Пришлось поправить переменную с путём к бинарю - ничего архитектурно сложного, но всё равно изменение, которое нужно раскатать.
systemd-timer вместо cron
Пока обновляли роль, решили заодно закрыть то, что давно чесалось: cron-задача для certbot renew - это не ужасно, но systemd-timer на CentOS 7 и современных Ubuntu даёт несколько реальных преимуществ, которые в нашем сценарии имеют значение.
Во-первых, логи. Cron пишет вывод в почту или в /var/log/cron в зависимости от дистрибутива и настройки. С systemd-timer весь вывод идёт в journald - journalctl -u certbot-renew.service показывает всё: когда запускался, что вернул, ошибки. Для 60+ доменов это важнее, чем для одного.
Во-вторых, статус. systemctl status certbot-renew.timer показывает когда запускался последний раз и когда следующий запуск. Не нужно лезть в crontab и считать следующее срабатывание вручную.
В-третьих, зависимости. Timer может быть привязан к network.target - не запустится, если сеть ещё не поднялась при старте системы. Cron в этом месте ни о чём не знает.
Структура получилась такая. Два файла в /etc/systemd/system/:
certbot-renew.service - unit самой задачи:
[Unit]
Description=Certbot renewal
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
certbot-renew.timer - расписание:
[Unit]
Description=Certbot renewal timer
[Timer]
OnCalendar=Wed,Sat 04:30
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true - важная деталь: если система была выключена в момент планового запуска, timer запустится при следующем старте системы. Cron это делать не умеет.
--post-hook вместо отдельной cron-строчки - certbot сам вызывает systemctl reload nginx только если хотя бы один сертификат был реально обновлён. Не каждый запуск, а только при обновлении. Это чище, чем безусловный reload после каждого certbot renew.
Включить и запустить:
systemctl daemon-reload
systemctl enable --now certbot-renew.timer
systemctl list-timers certbot-renew.timer
Последняя команда - для проверки: покажет next и last trigger.
Как дожали оставшийся список
За апрель-май мы закрыли оставшиеся домены. Часть из них - те самые нестандартные случаи.
HAProxy перед nginx. В апрельском посте мы описали временное решение через отдельный backend в HAProxy для challenge-пути. Теперь оформили это нормально: в Ansible-роль добавился вариант с haproxy_acl - если переменная letsencrypt_proxy: haproxy, роль прописывает в haproxy.cfg ACL и backend для .well-known/acme-challenge/ и делает reload HAProxy перед запросом сертификата. Не временный патч, а часть роли.
Много поддоменов на одном домене. Certbot принимает несколько -d в одной команде - один сертификат на весь список. Ограничение: не более 100 доменных имён в одном сертификате. До этого потолка мы нигде не дошли, но держим в голове.
DNS-валидация для одного закрытого стенда. Один клиент держит сервис без публичного HTTP - только VPN-доступ. Webroot там не работает. Использовали --preferred-challenges dns с ручным добавлением TXT-записи при первоначальном выпуске. Автопродление для такого стенда не автоматизировано - требует либо API к DNS-провайдеру, либо ручных действий раз в 90 дней. Оставили на ручном для этого одного случая, остальное - автомат.
Что получилось за месяц
За апрель-май прошли весь накопленный список. Больше 60 доменов на managed-сопровождении с автоматическим продлением через systemd-timer. Один ручной выпуск - тот самый закрытый стенд без публичного HTTP.
Несколько вещей, которые стали очевидны при таком масштабе.
Rate limits Let's Encrypt реальны. 20 сертификатов в неделю на один зарегистрированный домен - это не на домен вида example.ru, а на зарегистрированный домен (eTLD+1). Несколько клиентов с многодоменными конфигурациями на общих поддоменах потребовали планирования выдачи по дням, чтобы не упереться.
--quiet не значит «нет вывода при ошибке`. Certbot с --quiet молчит при успехе, но пишет ошибку при падении. Именно так и должно работать - но проверили на практике.
Мониторинг нужен отдельный. Systemd-timer фиксирует факт запуска и результат, но не предупредит, если сертификат не обновился и до истечения осталось 10 дней. Добавили в Zabbix проверку даты истечения через openssl s_client - алерт за 21 день. Это второй уровень страховки поверх автопродления.