TLS-сертификаты без боли: Caddy, certbot, acme.sh
Три способа получить сертификат Let's Encrypt и не вспоминать о нём, пока он сам продлевается. Плюс как проверить, что продление действительно работает.
Сертификат выпускается за минуту, а истекает всегда в пятницу вечером. Вся работа здесь не в выпуске, а в том, чтобы продление шло само, сервис подхватывал новый файл, и ты узнавал о проблеме раньше клиентов.
Какой инструмент выбрать
- Caddy, если он и так твой реверс-прокси. Сертификаты выпускает и продлевает сам, настраивать нечего.
- certbot, если стоит nginx и нужен сертификат на пару доменов.
- acme.sh, если нужен wildcard, порты 80/443 заняты (Xray, selfsteal) или сертификат нужен файлами для Xray и ноды.
Одно правило для всех: один домен обслуживает один ACME-клиент. В сообществе был случай, когда выпуск сертификата для кабинета через второй инструмент уронил панель в 500 ( база).
Caddy: автоматический HTTPS
Caddy выпускает сертификат для каждого домена, который встречает в конфиге, продлевает его и сам редиректит HTTP на HTTPS. Условия из документации:
- A/AAAA-записи домена указывают на сервер;
- порты 80 и 443 открыты снаружи, и Caddy может их слушать;
- каталог данных Caddy доступен на запись и не теряется между перезапусками;
- домен указан в конфиге.
{
email admin@example.com
}
panel.example.com {
reverse_proxy 127.0.0.1:8080
}systemctl reload caddy
journalctl -u caddy --no-pager | less +GПри установке из пакета Caddy работает от пользователя caddy, а сертификаты лежат в /var/lib/caddy/.local/share/caddy. В Docker это том /data.
Грабли
Контейнер Caddy без именованного тома на /data при каждом пересоздании выпускает сертификаты заново. Документация Caddy отдельно предупреждает: каталог данных не кеш. Частые docker compose up --force-recreate без тома быстро упираются в лимиты Let's Encrypt (см. ниже).
Если Let's Encrypt не выдал сертификат, Caddy по умолчанию попробует ZeroSSL. Поэтому письмо от Let's Encrypt «сертификат скоро истечёт» может быть ложной тревогой: смотри издателя в самом сертификате.
Wildcard в Caddy требует DNS-01, а для него нужна сборка Caddy с модулем своего DNS-провайдера из репозиториев caddy-dns. Для Cloudflare:
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy 127.0.0.1:8080
}Типовая грабля: Caddy не может выпустить сертификат, потому что домен не резолвится (NXDOMAIN) или бесплатный DNS-сервис не даёт выпустить сертификат на свой поддомен. Разборы в ошибках панелей. Примеры Caddyfile для панели, кабинета и вебхуков: Nginx / Caddy.
certbot + nginx
Плагин nginx сам выпускает сертификат и прописывает его в конфиг:
certbot --nginx -d panel.example.comЕсли не хочешь, чтобы certbot трогал конфиги, используй webroot: certbot кладёт файл проверки в <webroot>/.well-known/acme-challenge/, а nginx должен его отдать по HTTP на порту 80.
server {
listen 80;
server_name panel.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}certbot certonly --webroot -w /var/www/letsencrypt -d panel.example.com \
--deploy-hook "systemctl reload nginx"Файлы появятся в /etc/letsencrypt/live/panel.example.com/: fullchain.pem и privkey.pem. Указывай на них напрямую, не копируй: при продлении certbot обновляет именно эти пути.
Продление и deploy-hook
Большинство способов установки certbot сами ставят таймер или cron на certbot renew. Проверь:
systemctl list-timers | grep -i certbot
grep -r certbot /etc/crontab /etc/cron.d/ 2>/dev/nullcertbot renew продлевает только то, что пора продлевать (с certbot 4.0, когда осталось меньше трети срока), поэтому запускать его можно хоть дважды в день. Недельный cron для этого слишком редкий, на что в сообществе уже наступали (
база).
--deploy-hook выполняется только после успешного выпуска и сохраняется для следующих продлений. Для хука на все сертификаты сразу положи исполняемый файл в каталог:
#!/bin/sh
# $RENEWED_LINEAGE: например /etc/letsencrypt/live/panel.example.com
systemctl reload nginx
# если сервис в контейнере не перечитывает сертификат сам, перезапусти его
# docker restart remnanodechmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload.sh
certbot renew --dry-runЗаметка
--dry-run ходит в staging Let's Encrypt, у которого лимиты заметно выше, и боевые лимиты не трогает. Deploy-хуки при этом не запускаются, если не добавить --run-deploy-hooks. Проверь их отдельно: certbot renew --dry-run --run-deploy-hooks.
Список сертификатов и сроки: certbot certificates.
acme.sh: wildcard через Cloudflare DNS
DNS-01 не нужны открытые порты: подтверждение идёт TXT-записью. Это единственный способ получить wildcard у Let's Encrypt.
curl https://get.acme.sh | sh -s email=admin@example.com
# после установки перезайди в shell, чтобы появился alias acme.sh
acme.sh --set-default-ca --server letsencryptВажно
С версии 3.0 CA по умолчанию у acme.sh не Let's Encrypt, а ZeroSSL. Если нужен Let's Encrypt, выполни --set-default-ca до первого выпуска.
Токен создай в Cloudflare (Profile → API Tokens) с правом Zone → DNS → Edit только на нужную зону. Global API Key не используй: его утечка отдаёт весь аккаунт.
export CF_Token="токен"
export CF_Zone_ID="id-зоны" # для одной зоны
# или CF_Account_ID="id-аккаунта" # если зон несколько в одном аккаунте
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'Zone ID и Account ID видны на странице Overview зоны справа, в блоке API. Переменные acme.sh сохранит в ~/.acme.sh/account.conf и переиспользует при продлении, заново экспортировать не придётся.
В примере из wiki указаны оба имени, и сам домен, и wildcard: нужен сертификат на оба, перечисляй оба.
--install-cert и reloadcmd
Файлы из ~/.acme.sh/ использовать напрямую нельзя: README говорит, что это внутренняя структура и она может измениться. Копируем туда, откуда их читает сервис:
acme.sh --install-cert -d example.com \
--key-file /etc/ssl/example.com/privkey.pem \
--fullchain-file /etc/ssl/example.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"Параметры запоминаются: при каждом продлении acme.sh скопирует файлы заново и выполнит --reloadcmd. Без reloadcmd новый сертификат ляжет на диск, а сервис продолжит отдавать старый до истечения.
Установщик acme.sh ставит ежедневный cron. Проверить и посмотреть дату следующего продления:
crontab -l | grep acme
acme.sh --info -d example.com | grep Le_NextRenewTimeStrЛайфхак
Один wildcard на все ноды удобен при selfsteal и DNS-балансировке, а раскатывать его по нодам можно через --reloadcmd со скриптом копирования. Подробнее о практике в
базе. Помни, что ключ от wildcard на каждой ноде увеличивает цену утечки, а CF_Token на ноде даёт право менять DNS.
Проверка срока и цепочки
Что отдаёт сервер на самом деле, а не что лежит на диске:
echo | openssl s_client -connect panel.example.com:443 -servername panel.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName-servername задаёт SNI. Без него OpenSSL берёт имя из -connect, а при подключении по IP SNI не будет, и сервер может отдать не тот сертификат. Цепочка, которую отдаёт сервер, и её проверка:
# что именно прислал сервер, по порядку
echo | openssl s_client -connect panel.example.com:443 -servername panel.example.com -showcerts
# проверка цепочки: с -verify_return_error рукопожатие оборвётся на ошибке
echo | openssl s_client -connect panel.example.com:443 -servername panel.example.com \
-verify_return_error -briefЕсли в конфиге nginx указан cert.pem вместо fullchain.pem, сервер не отдаёт промежуточный сертификат. Документация certbot: для ssl_certificate в nginx нужен именно fullchain.pem, иначе часть браузеров покажет недоверенное соединение.
Для мониторинга подходит -checkend: код возврата не ноль, если сертификат истечёт в ближайшие N секунд.
echo | openssl s_client -connect panel.example.com:443 -servername panel.example.com 2>/dev/null \
| openssl x509 -noout -checkend 1209600 || echo "меньше 14 дней"acme.sh сам не пишет о скором истечении, так что такую проверку с алертом в Telegram стоит завести отдельно ( база).
Лимиты Let's Encrypt
Актуальные цифры с letsencrypt.org на момент написания:
| Лимит | Значение |
|---|---|
| Сертификатов на зарегистрированный домен | 50 за 7 дней |
| Сертификатов на один и тот же набор имён | 5 за 7 дней |
| Неудачных проверок на имя с одного аккаунта | 5 в час |
| Новых заказов на аккаунт | 300 за 3 часа |
Лимиты работают как ведро с токенами и восстанавливаются постепенно, сбросить их вручную нельзя. Продления через ARI (acme.sh использует его автоматически) от лимитов освобождены полностью. Обычное продление с тем же набором имён освобождено от лимитов на домен и на аккаунт, но не от лимита «5 на тот же набор». Для экспериментов используй staging: --dry-run у certbot, --server letsencrypt_test у acme.sh.
Не делай так
Не завязывайся на «сертификат живёт 90 дней». Let's Encrypt сокращает срок: с 13 мая 2026 профиль tlsserver выдаёт 45-дневные сертификаты, с 10 февраля 2027 классический профиль перейдёт на 64 дня, с 16 февраля 2028 на 45. Самописный cron «раз в 60 дней» скоро перестанет успевать. Пусть продлением управляет клиент.
Где смотреть выпущенные сертификаты
Каждый публичный сертификат попадает в логи Certificate Transparency. Поиск по ним: crt.sh и Censys. Так видно, не выпускал ли кто-то сертификат на твой домен и какие поддомены уже «засвечены»: wildcard их не прячет, поддомены всё равно видны в SNI.
В досье домена на host.pink, например host.pink/example.com, есть карточка «Сертификаты и поддомены» с тем же из CT-логов.
Источники
- Caddy: Automatic HTTPS
- Caddy: tls directive
- Caddy: Conventions (data directory)
- Caddy: Install
- Docker Official Image: caddy
- Certbot User Guide
- acme.sh README
- acme.sh wiki: dnsapi (Cloudflare)
- acme.sh wiki: Server, Change default CA to ZeroSSL
- Let's Encrypt: Rate Limits
- Let's Encrypt: Challenge Types
- Let's Encrypt: FAQ
- Let's Encrypt: Decreasing Certificate Lifetimes to 45 Days
- OpenSSL: openssl-s_client
- OpenSSL: openssl-x509