Let's Encrypt и certbot: автообновление сертификатов на VPS
Let's Encrypt — бесплатный, рабочий, проверенный. Сертификаты выдаются на 90 дней, обновляются автоматически. И всё равно у меня раз в полгода кто-нибудь жалуется, что «сайт показывает истёкший сертификат». В девяти случаях из десяти — настройка обновления сделана криво. Расскажу, как сделать ровно один раз и забыть.
Контекст: Ubuntu 24.04 на VPS, nginx, certbot из дистрибутива (1.x). На Debian 12 и Astra Linux логика та же.
Установка
На свежем Ubuntu есть два варианта: пакет certbot с плагином python3-certbot-nginx или snap-версия. Я предпочитаю первый, потому что не люблю snap на серверах:
sudo apt update
sudo apt install -y certbot python3-certbot-nginxВерсия в Ubuntu 24.04 — 2.x, она современная и поддерживает все нужные мне фичи: ECDSA-ключи, ACMEv2, профили, DNS-01.
Первый сертификат через nginx-плагин
Самый простой путь, если nginx уже стоит и слушает 80/443:
sudo certbot --nginx -d app.example.ru -d www.app.example.ru \
--email admin@example.ru \
--agree-tos --no-eff-email \
--redirectЧто делает:
- Запрашивает сертификат у Let's Encrypt.
- Сам редактирует nginx-конфиг, добавляя
ssl_certificateи редирект http→https. - Перезагружает nginx.
Для большинства типовых сценариев этого достаточно. Я однако предпочитаю руками держать nginx-конфиг и не давать certbot его трогать.
Webroot — мой выбор
Подход с webroot не трогает конфиг nginx и работает по принципу «certbot кладёт challenge-файл, nginx раздаёт его как статику».
В /etc/nginx/sites-available/myapp.conf в HTTP-блоке должен быть:
server {
listen 80;
listen [::]:80;
server_name app.example.ru;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}Создать каталог:
sudo mkdir -p /var/www/certbot
sudo chown -R www-data:www-data /var/www/certbotПолучить сертификат:
sudo certbot certonly --webroot \
-w /var/www/certbot \
-d app.example.ru -d www.app.example.ru \
--email admin@example.ru \
--agree-tos --no-eff-email \
--key-type ecdsa--key-type ecdsa — поведение Let's Encrypt по умолчанию даёт RSA. ECDSA-ключи короче, быстрее и поддерживаются всеми современными браузерами. На моих сервисах TLS-handshake с ECDSA измеримо быстрее.
После успешного запуска появляются файлы:
/etc/letsencrypt/live/app.example.ru/
cert.pem
chain.pem
fullchain.pem
privkey.pemВ nginx подключаем fullchain.pem как ssl_certificate и privkey.pem как ssl_certificate_key.
Автообновление
Тут начинается самое интересное и где чаще всего ломается.
В Ubuntu 22.04 и новее с пакетом certbot уже идёт systemd-таймер. Проверить:
systemctl list-timers | grep certbotДолжно быть что-то вроде certbot.timer с расписанием раз в 12 часов. Сам сервис certbot.service запускает certbot renew.
На Ubuntu 20.04 и более старых системах был cron-файл /etc/cron.d/certbot. Если у тебя смешанная инфраструктура, не лишне проверить, что какой-то один механизм работает.
Хук перезагрузки
certbot обновляет сертификат, но nginx по-прежнему держит в памяти старый. Нужно дать сигнал. Это делается через deploy-hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shХук вызывается только если сертификат реально обновился. То есть таймер пинает certbot каждые 12 часов, но реальная работа (и reload nginx) случается за 30 дней до истечения.
Проверка
sudo certbot renew --dry-runЭта команда честно проигрывает обновление, не выдавая нового сертификата. Если всё корректно настроено — увидишь «Congratulations, all renewals succeeded». Если нет — будет ошибка с конкретным доменом.
Дополнительно я смотрю, что таймер стоит и работает:
systemctl status certbot.timer
systemctl status certbot.service
journalctl -u certbot -n 50Wildcard-сертификат через DNS-01
Если нужен *.example.ru, обычный HTTP-01 не подойдёт: Let's Encrypt требует доказать владение через DNS. Это означает заведение TXT-записи. Вариантов два:
- Руками каждый раз через
--preferred-challenges dns --manual. Не годится для авто-обновления. - DNS-плагин для твоего регистратора. Cloudflare, Yandex, AWS Route53 и пр.
Например, для Cloudflare:
sudo apt install python3-certbot-dns-cloudflare
sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null <<EOF
dns_cloudflare_api_token = <your_token_with_zone_dns_edit>
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d 'example.ru' -d '*.example.ru' \
--email admin@example.ru \
--agree-tos --no-eff-email \
--key-type ecdsaAPI-токен делаешь с минимальными правами: только Zone:DNS:Edit на нужную зону. Не забывай chmod 600 на файл с токеном.
Для регистраторов, у которых нет certbot-плагина, есть универсальный путь — acme.sh: shell-клиент, поддерживающий десятки DNS-провайдеров. На Ubuntu я обычно беру certbot, на экзотике — acme.sh.
Несколько доменов в одном сертификате
Перечисляешь через -d. Можно держать до 100 имён в одном сертификате — формальное ограничение Let's Encrypt. На практике я предпочитаю один сертификат на один логический сервис: проще ротация, меньше радиус взрыва.
Пример: один сервис обслуживает app.example.ru и API на api.example.ru. Кладу в один сертификат — оба обновляются вместе.
Подводные камни
Открытый 80 порт
Для HTTP-01 challenge порт 80 должен быть доступен снаружи. Если ты закрыл его в файрволе «потому что используем только https» — обновление не пройдёт. Я держу 80 открытым для редиректа на 443 и для challenges.
Кеширующий CDN
Если перед сервером Cloudflare или Yandex Cloud CDN, и они кэшируют ответы на /.well-known/acme-challenge/... — challenge не дойдёт. Решение: либо использовать DNS-01, либо настроить bypass cache на этом пути.
Лимиты Let's Encrypt
На один домен — максимум 50 сертификатов в неделю, 5 дублирующих за 7 дней. На стейджинге легко вписаться при тестировании. Используй --staging для отладочных запросов: лимиты гораздо мягче, сертификаты — невалидные с точки зрения браузера, но процесс отрабатывается.
sudo certbot certonly --staging --webroot ...
# когда всё работает, делаешь то же самое без --stagingИстёк токен у DNS-провайдера
Незаметная проблема. Сертификат не обновляется, реал клиенты ничего не замечают, пока не подходит конец 90 дней. Я на каждом сервере держу cron, который раз в неделю проверяет дату истечения сертификата и алертит, если осталось меньше 25 дней:
expiry=$(echo | openssl s_client -servername app.example.ru -connect app.example.ru:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
left=$(( ( $(date -d "$expiry" +%s) - $(date +%s) ) / 86400 ))
[ $left -lt 25 ] && echo "cert expires in $left days" | mail -s alert me@example.ruУдаление сертификата
Когда домен больше не нужен, certbot умеет почистить:
sudo certbot certificates # список
sudo certbot delete --cert-name app.example.ruУдалить также записи в /etc/letsencrypt/renewal/*.conf, иначе certbot будет пытаться обновить уже не существующий сертификат и засорять журнал.
Шпаргалка
- Используй webroot или DNS-01, не --nginx (лучше управлять конфигом руками).
- Ключ ECDSA: короче, быстрее.
- Deploy-hook на reload nginx.
- Проверяй
certbot renew --dry-runна свежем сервере. - Открывай 80 порт. CDN — bypass для acme-challenge или DNS-01.
- Алерт на дату истечения как страховка.
- На staging тренируйся через
--staging.
Один раз настроенный certbot работает годами без вмешательства. Главное — не забывать deploy-hook и иметь страховочный алерт. Я с этим набором живу с 2018-го, и реальный сбой автообновления у меня случался ровно один раз — когда добавил CDN перед сайтом и не настроил bypass.