Веб-сервер Caddy 2 заслужил репутацию самого удобного инструмента для автоматического управления HTTPS-сертификатами: достаточно указать доменное имя в конфигурационном файле, и Caddy самостоятельно связывается с Let's Encrypt или ZeroSSL, проходит проверку владения доменом и настраивает шифрование. Однако в стандартной конфигурации эта магия работает строго по протоколу HTTP-01 challenge, требующему прямого доступа к серверу из интернета по незащищенному 80 порту.
Как только перед системным администратором встает задача выпустить Wildcard SSL-сертификат (*.domain.ru) для десятка внутренних сервисов, защитить сервер в закрытом контуре за NAT или скрыть админ-панели без публикации служебных портов в глобальную сеть, стандартный механизм дает сбой. Единственным надежным решением становится переход на ACME DNS-01 challenge либо развертывание собственного удостоверяющего центра Internal CA.
В этом практическом руководстве подробно разбирается архитектура DNS-валидации, сборка кастомного Docker-образа Caddy с официальными плагинами Cloudflare и DuckDNS через multi-stage сборщик xcaddy, безопасная изоляция служебных контейнеров от сканирования и выпуск Wildcard-сертификатов без необходимости держать открытым 80 порт.
1. Почему стандартный Caddy не может выпустить Wildcard SSL без DNS-01
Чтобы понять необходимость сборки кастомного бинарника Caddy, разберем фундаментальную разницу между двумя основными типами проверок центра сертификации Let's Encrypt согласно спецификации RFC 8555:
| Параметр | HTTP-01 Challenge | DNS-01 Challenge | Internal CA (tls internal) |
|---|---|---|---|
| Требуемые порты | Порт 80/tcp обязан быть открыт наружу |
Порты 80 и 443 могут быть полностью закрыты |
Внешние порты не требуются |
| Поддержка Wildcard | Не поддерживается (запрещено RFC) | Полная поддержка *.domain.ru |
Полная поддержка любых доменов |
| Работа за NAT / VPN | Требует белый IP и проброс портов | Работает за NAT, VPN и серыми IP | Работает в полностью изолированной сети |
| Доверие браузеров | Автоматическое (Let's Encrypt / ZeroSSL) | Автоматическое (Let's Encrypt / ZeroSSL) | Требуется импорт корневого сертификата |
При стандартной проверке HTTP-01 сервер Let's Encrypt выполняет HTTP-запрос по адресу http://domain.ru/.well-known/acme-challenge/{token}. Если порт 80 заблокирован интернет-провайдером, закрыт системным фаерволом или сервер расположен в закрытом контуре компании, проверка завершается критической ошибкой Connection refused или Timeout during connect.
В случае с DNS-01 challenge физическое подключение к вашему серверу не требуется вообще. Caddy связывается с API вашего DNS-хостинга (Cloudflare или DuckDNS) и автоматически публикует специальную криптографическую TXT-запись вида _acme-challenge.domain.ru. Роботы Let's Encrypt опрашивают публичные неймсерверы, проверяют значение TXT-записи и немедленно подписывают сертификат. Вы можете в любой момент верифицировать видимость DNS-записей через сетевой резолвер SysKit DNS Lookup.
Почему официальный образ caddy:alpine не содержит DNS-модулей
Команда разработчиков Caddy намеренно не включает в стандартный бинарник библиотеки интеграции с сотнями DNS-провайдеров. В противном случае размер исполняемого файла превысил бы 500 МБ из-за сотен внешних зависимостей на Go. Для подключения Cloudflare или DuckDNS официальная документация предписывает собирать легковесный кастомный образ через утилиту xcaddy.
2. Получение API-токенов в Cloudflare и DuckDNS
Перед началом настройки контейнеров необходимо получить API-ключи, предоставляющие Caddy права на создание и удаление валидационных TXT-записей в DNS-зоне.
Вариант А: Выпуск ограниченного API Token в Cloudflare
Категорически запрещено использовать устаревший глобальный ключ Global API Key, так как его утечка скомпрометирует весь аккаунт Cloudflare. Создаем токен с минимальными привилегиями по принципу наименьших прав (Least Privilege):
- Авторизуйтесь в панели Cloudflare и перейдите в раздел My Profile → API Tokens.
- Нажмите Create Token и выберите шаблон Edit zone DNS (или создайте Custom Token).
- В блоке Permissions задайте два правила:
Zone — DNS — Edit(право создания и обновления TXT-записей);Zone — Zone — Read(право чтения параметров зоны для определения ID домена).
- В блоке Zone Resources выберите
Include — Specific zone — ваш-домен.ru. Это изолирует токен строго в рамках одного домена. - Нажмите Continue to summary, затем Create Token и сохраните сгенерированный ключ.
Вариант Б: Получение токена в сервисе DuckDNS
Если у вас нет собственного домена второго уровня и вы используете бесплатный динамический DNS для тестового сервера или лабораторного стенда:
- Авторизуйтесь на официальном сайте
duckdns.orgчерез GitHub или Google. - Создайте желаемый субдомен (например,
myhomelab.duckdns.org). - В верхней части панели скопируйте постоянный строковый token аккаунта (UUID). DuckDNS поддерживает установку TXT-записей через официальный API для субдоменов.
Создайте на вашем VDS рабочую директорию проекта и сохраните токены в изолированный файл окружения .env с ограниченными правами доступа:
mkdir -p /opt/caddy-gateway && cd /opt/caddy-gateway
# Создаем файл переменных окружения
cat << 'EOF' > .env
CLOUDFLARE_API_TOKEN=v8xK9...ваш_секретный_токен_cloudflare...
DUCKDNS_API_TOKEN=3a7b1c4d-...ваш_токен_duckdns...
EOF
# Защищаем файл от чтения другими пользователями системы
chmod 600 .env
3. Multi-stage Dockerfile: сборка Caddy 2.10 с плагинами через xcaddy
Для создания компактного production-образа используем официальный паттерн многоэтапной сборки (Multi-stage build) на базе актуальной стабильной линейки Caddy 2.10.x. На первом этапе образ caddy:2.10.2-builder компилирует модифицированный исполняемый файл Caddy с подключаемыми модулями через инструмент xcaddy. На втором этапе бинарник копируется в чистый минималистичный образ caddy:2.10.2-alpine, исключая компилятор Go и исходный код сборщика.
Создайте файл Dockerfile в каталоге /opt/caddy-gateway/Dockerfile:
# Этап 1: Сборка кастомного бинарника Caddy 2.10 с DNS-плагинами
FROM caddy:2.10.2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/caddy-dns/duckdns
# Этап 2: Финальный легковесный образ среды исполнения
FROM caddy:2.10.2-alpine
# Копируем скомпилированный бинарник Caddy поверх стандартного
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Совет инженера: фиксация версий ПО
Всегда фиксируйте конкретный минорный релиз (в данном примере 2.10.2) вместо плавающего тега latest. При автоматическом обновлении контейнеров тег latest может затянуть мажорный апдейт с ломающими изменениями в синтаксисе плагинов.
4. Развертывание в Docker Compose с лимитами cgroups и сохранением ключей
При развертывании обратного прокси Caddy в Docker Compose необходимо строго соблюдать два критических требования безопасности и надежности:
- Персистентность каталога
/data: Caddy сохраняет учетные записи ACME, приватные TLS-ключи и выпущенные сертификаты в директории/data. Если не примонтировать постоянный том, при каждом перезапуске контейнера сертификаты будут запрашиваться заново, что неминуемо приведет к блокировке домена по строгим недельным лимитам Let's Encrypt (Rate Limits). - Защита служебных портов: сервисы, скрывающиеся за Caddy (Grafana, Vaultwarden, базы данных, админки), не должны публиковать порты на
0.0.0.0. Caddy маршрутизирует трафик по внутренней изолированной сети Docker Bridge по именам контейнеров, блокируя обход системного фаервола.
В современной спецификации Compose Specification (Docker Compose v2+) ограничения ресурсов ядра Linux (cgroups v1/v2) задаются лаконично через директивы верхнего уровня mem_limit и cpus:
services:
caddy:
build:
context: .
dockerfile: Dockerfile
container_name: caddy-ssl-gateway
restart: unless-stopped
mem_limit: 512m
cpus: 1.0
env_file:
- .env
ports:
# Порт 80 опционален (нужен только если требуется редирект HTTP -> HTTPS)
- "80:80"
- "443:443"
- "443:443/udp" # Поддержка HTTP/3 (QUIC)
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web-gateway
# Демонстрационный бэкенд (порт наружу не публикуется!)
demo-app:
image: traefik/whoami:latest
container_name: demo-internal-app
restart: unless-stopped
mem_limit: 128m
cpus: 0.5
networks:
- web-gateway
networks:
web-gateway:
name: web-gateway
driver: bridge
volumes:
caddy_data:
name: caddy_data
caddy_config:
name: caddy_config
Стандарт лимитов в современной Compose Specification
В современных версиях Docker Compose v2+ директивы mem_limit и cpus задаются непосредственно на верхнем уровне каждого сервиса. Они напрямую транслируются ядром Linux в ограничения cgroups без устаревших громоздких блоков deploy.resources.limits, которые ранее требовались для Swarm.
После развертывания обязательно проверьте состояние сетевых портов сервера с помощью сканера SysKit Port Scanner, чтобы убедиться, что внутренний сервис whoami недоступен напрямую из внешней сети в обход шлюза шифрования.
5. Настройка Caddyfile: раздельные сертификаты и маршрутизация *.домен
Важный архитектурный нюанс Caddy 2: при объявлении связки *.syskit-demo.ru, syskit-demo.ru сервер не запрашивает один общий SAN-сертификат (как это делает утилита Certbot с флагом -d example.com -d *.example.com).
Вместо этого автоматический TLS-модуль Caddy инициирует выпуск и сопровождение двух независимых сертификатов:
- Первый сертификат выпускается строго для маски
*.syskit-demo.ruчерез DNS-01 challenge; - Второй сертификат выпускается персонально для корневого домена
syskit-demo.ru(также через DNS-01).
При входящем клиентском подключении Caddy считывает расширение SNI (Server Name Indication) в заголовке TLS-рукопожатия и автоматически отдает соответствующий сертификат. С точки зрения администрирования это происходит полностью прозрачно, однако при планировании квот и лимитов Let's Encrypt (50 сертификатов на зарегистрированный домен в неделю) необходимо учитывать, что списывается два запроса на выпуск, а не один.
Создайте конфигурационный файл Caddyfile:
# Глобальный блок настроек
{
email admin@syskit-demo.ru
# При отладке раскомментируйте тестовый CA во избежание блокировок:
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
# Повторно используемый сниппет для Cloudflare DNS-01
(cloudflare_tls) {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
resolvers 1.1.1.1 1.0.0.1
}
}
# Единый блок для корневого домена и всех субдоменов
*.syskit-demo.ru, syskit-demo.ru {
import cloudflare_tls
# Маршрутизация субдомена app.syskit-demo.ru на внутренний контейнер
@app host app.syskit-demo.ru
handle @app {
reverse_proxy demo-app:80
}
# Маршрутизация панели мониторинга
@monitor host mon.syskit-demo.ru
handle @monitor {
reverse_proxy host.docker.internal:3000
}
# Ответ по умолчанию для корневого домена и остальных субдоменов
handle {
respond "SysKit Caddy Wildcard Gateway: HTTPS активен без открытия 80 порта!" 200
}
}
Почему важно указывать резолверы 1.1.1.1
Параметр resolvers 1.1.1.1 1.0.0.1 заставляет Caddy опрашивать публичные авторитетные DNS-серверы Cloudflare напрямую при верификации создания TXT-записи. Если этот параметр опущен, Caddy использует локальный резолвер хоста (например, systemd-resolved или DNS провайдера), который может кэшировать отрицательный ответ (NXDOMAIN) и приводить к фатальной ошибке тайм-аута валидации.
Конфигурация для субдоменов DuckDNS
Если вы настраиваете сертификат для субдомена DuckDNS, блок конфигурации выглядит следующим образом с использованием плагина duckdns:
*.myhomelab.duckdns.org {
tls {
dns duckdns {env.DUCKDNS_API_TOKEN}
resolvers 8.8.8.8
}
reverse_proxy demo-app:80
}
Критическое ограничение DuckDNS: одна TXT-запись на домен
API сервиса DuckDNS исторически поддерживает хранение только одной TXT-записи на субдомен. Если вы укажете в Caddyfile связку *.myhomelab.duckdns.org, myhomelab.duckdns.org, Caddy попытается параллельно запросить два сертификата и отправить две TXT-записи _acme-challenge. Вторая запись мгновенно перезапишет первую в базе DuckDNS, что вызовет сбой валидации Let's Encrypt (Incorrect TXT record).
Для стабильной работы с DuckDNS объявляйте строго одну маску *.myhomelab.duckdns.org, либо используйте DNS Cloudflare для продакшен-окружений.
Запустите сборку и старт стека командой:
docker compose up -d --build
# Проверяем логи прохождения DNS-01 валидации
docker compose logs -f caddy
В логах отобразится процесс получения сертификатов: Caddy обратится к API Cloudflare, добавит TXT-запись _acme-challenge, дождется подтверждения от Let's Encrypt и сохранит полученную цепочку в том caddy_data. Корректность выпущенного сертификата можно мгновенно верифицировать через инструмент SysKit SSL Checker.
6. Локальный CA (tls internal) для закрытых контейнеров и сетей
Бывают ситуации, когда сервисы функционируют в полностью изолированной корпоративной сети без доступа к внешним DNS-серверам или на локальных именах вроде vault.home.local. Публичный центр Let's Encrypt принципиально не выдает сертификаты на несуществующие доменные зоны.
Для таких задач в Caddy встроен собственный удостоверяющий центр Internal CA, активируемый одной директивой tls internal:
# Приватный контур без выхода в публичный DNS
vault.corp.local, api.corp.local {
tls internal
@vault host vault.corp.local
handle @vault {
reverse_proxy vaultwarden:80
}
handle {
respond "Доступ только для внутренней сети компании" 403
}
}
При первом запуске Caddy автоматически сгенерирует собственный корневой сертификат (Root CA) и промежуточный центр сертификации. Чтобы браузеры и операционные системы перестали выдавать предупреждение о ненадежном соединении (NET::ERR_CERT_AUTHORITY_INVALID), необходимо единожды установить корневой сертификат Caddy в доверенные хранилища хостов:
# Извлекаем сгенерированный корневой сертификат из Docker-тома
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root.crt
# Установка в доверенные центры на Linux (Ubuntu / Debian):
sudo cp /tmp/caddy-root.crt /usr/local/share/ca-certificates/caddy-internal-root.crt
sudo update-ca-certificates
# Установка в Windows (PowerShell от имени Администратора):
# Import-Certificate -FilePath "C:caddy-root.crt" -CertStoreLocation Cert:LocalMachineRoot
7. Диагностика DNS-записей, ограничение DuckDNS и ошибки rate limit
При настройке DNS-валидации администраторы чаще всего сталкиваются со следующими типовыми ошибками:
1. Ошибка Invalid token или Unauthorized
В логах Caddy возникает сообщение: HTTP 401 Unauthorized: Invalid API Token. Это означает, что токен скопирован с пробелом, истек срок его действия либо в панели Cloudflare не выданы права Zone.Zone:Read. Плагин обязан уметь считывать Zone ID домена, чтобы адресовать запрос на добавление TXT-записи в правильную зону.
2. Ошибка CAA record does not allow issuance
Если для вашего домена настроены записи DNS CAA (Certification Authority Authorization), они строго регламентируют, какие удостоверяющие центры вправе подписывать сертификаты. Если там прописан только digi-cert, Let's Encrypt отклонит заявку. Проверьте записи домена:
dig CAA syskit-demo.ru +short
Для корректной работы Let's Encrypt и ZeroSSL добавьте записи: syskit-demo.ru. IN CAA 0 issue "letsencrypt.org" и syskit-demo.ru. IN CAA 0 issue "zerossl.com".
3. Превышение лимитов Let's Encrypt (Rate Limits)
Let's Encrypt ограничивает выпуск 50 сертификатов на зарегистрированный домен в неделю. Поскольку Caddy выпускает отдельные сертификаты для *.domain.ru и domain.ru, расходуется два сертификата. При повторных экспериментах с конфигурацией всегда временно включайте тестовый сервер Let's Encrypt Staging:
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
Важное предупреждение: регулярный бэкап тома caddy_data
Никогда не выполняйте команду docker volume rm caddy_data без предварительного резервного копирования. Потеря закрытых ключей учетной записи и сертификатов вынудит сервер запрашивать перевыпуск с нуля, что при активных тестах приведет к временной блокировке домена со стороны Let's Encrypt на 7 дней.
Рекомендуемые VDS-провайдеры
Отказоустойчивые сервера с быстрыми NVMe и каналом до 1 Гбит/с
Timeweb Cloud — Быстрые NVMe VDS
Идеальная площадка для запуска Docker-инфраструктуры и обратного прокси Caddy: быстрые NVMe-диски, дата-центры в РФ и Европе с минимальным пингом и круглосуточная поддержка 24/7.
Selectel — Надежность Tier III
Отказоустойчивые серверы корпоративного уровня со стабильной сетевой связностью и гарантированным SLA 99.98% для критически важных шлюзов и баз данных.
Beget — Простой старт
Удобные облачные серверы с моментальным созданием, автоматическим резервным копированием и чистыми IPv4-адресами для веб-серверов и микросервисов.
Разверните отказоустойчивый шлюз на быстром NVMe VDS
Для надежной работы Docker Compose, бесперебойного автопродления сертификатов и высокоскоростного проксирования трафика с защитой от DDoS-атак выбирайте проверенные облачные серверы от ведущих провайдеров.