Руководство 9 мин чтения 2026-09-26

Caddy 2 с Wildcard SSL через Cloudflare и DuckDNS: выпуск сертификатов без открытия 80 порта и локальный CA для Docker

Как выпустить Wildcard SSL-сертификат (*.домен) в Caddy 2 через DNS-01 challenge в Cloudflare и DuckDNS. Сборка Dockerfile с плагинами, настройка без открытия 80 порта наружу и локальный CA для закрытого контура.

Инженерная редакция SysKit Проверено инженерами SysKit.ru

Веб-сервер 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):

  1. Авторизуйтесь в панели Cloudflare и перейдите в раздел My Profile → API Tokens.
  2. Нажмите Create Token и выберите шаблон Edit zone DNS (или создайте Custom Token).
  3. В блоке Permissions задайте два правила:
    • Zone — DNS — Edit (право создания и обновления TXT-записей);
    • Zone — Zone — Read (право чтения параметров зоны для определения ID домена).
  4. В блоке Zone Resources выберите Include — Specific zone — ваш-домен.ru. Это изолирует токен строго в рамках одного домена.
  5. Нажмите Continue to summary, затем Create Token и сохраните сгенерированный ключ.

Вариант Б: Получение токена в сервисе DuckDNS

Если у вас нет собственного домена второго уровня и вы используете бесплатный динамический DNS для тестового сервера или лабораторного стенда:

  1. Авторизуйтесь на официальном сайте duckdns.org через GitHub или Google.
  2. Создайте желаемый субдомен (например, myhomelab.duckdns.org).
  3. В верхней части панели скопируйте постоянный строковый 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 необходимо строго соблюдать два критических требования безопасности и надежности:

  1. Персистентность каталога /data: Caddy сохраняет учетные записи ACME, приватные TLS-ключи и выпущенные сертификаты в директории /data. Если не примонтировать постоянный том, при каждом перезапуске контейнера сертификаты будут запрашиваться заново, что неминуемо приведет к блокировке домена по строгим недельным лимитам Let's Encrypt (Rate Limits).
  2. Защита служебных портов: сервисы, скрывающиеся за 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.

Конфигурация
1 vCPU 3.3 ГГц • 1 ГБ RAM • 15 ГБ NVMe • Трафик без ограничений
Премиум Tier III
🔷

Selectel — Надежность Tier III

Отказоустойчивые серверы корпоративного уровня со стабильной сетевой связностью и гарантированным SLA 99.98% для критически важных шлюзов и баз данных.

Конфигурация
от 1–2 vCPU • 2–4 ГБ RAM • 30 ГБ NVMe • Доступность SLA 99.98%
KVM / Авто-бэкапы
🟠

Beget — Простой старт

Удобные облачные серверы с моментальным созданием, автоматическим резервным копированием и чистыми IPv4-адресами для веб-серверов и микросервисов.

Конфигурация
1 vCPU • 1 ГБ RAM • 20 ГБ NVMe • Выделенный статический IPv4

Разверните отказоустойчивый шлюз на быстром NVMe VDS

Для надежной работы Docker Compose, бесперебойного автопродления сертификатов и высокоскоростного проксирования трафика с защитой от DDoS-атак выбирайте проверенные облачные серверы от ведущих провайдеров.

Вопросы и ответы (FAQ)

Популярные вопросы вебмастеров и сисадминов по данной теме

Можно ли использовать Caddy DNS-01 на сервере вообще без белого IP-адреса и за NAT?
Да, это ключевое преимущество протокола DNS-01 challenge. Let's Encrypt проверяет не доступность вашего IP-адреса по портам 80 или 443, а наличие валидной TXT-записи в DNS-зоне через API Cloudflare или DuckDNS. Сервер может находиться за NAT, в локальной сети или за VPN без публичного IP.
Покрывает ли Wildcard-сертификат *.domain.ru сам корневой домен domain.ru и как этим управляет Caddy?
По стандарту RFC 6125 маска *.domain.ru защищает только субдомены первого уровня (например, api.domain.ru или panel.domain.ru) и не покрывает корневой домен domain.ru. При указании *.domain.ru, domain.ru в Caddyfile сервер Caddy автоматически выпускает и сопровождает два независимых сертификата — один для wildcard-маски и один для корня, автоматически отдавая нужный клиенту по SNI.
Как переключиться на тестовый сервер Let's Encrypt Staging, чтобы не исчерпать лимиты запросов?
В начале Caddyfile в блоке глобальных настроек добавьте директиву acme_ca https://acme-staging-v02.api.letsencrypt.org/directory. Staging-сервер выдает тестовые сертификаты (браузер покажет предупреждение о ненадежном сертификате Fake LE), но лимиты запросов там в десятки раз выше, что идеально подходит для отладки Dockerfile и токенов.

Читайте также в блоге SysKit

Смежные руководства и полезные технические статьи