Зачем DNS-01 для wildcard и чем он сложнее «обычного» ACME
Wildcard-сертификат (например, *.example.com) в Let’s Encrypt выпускается только через проверку DNS-01. В отличие от HTTP-01/TLS-ALPN-01, здесь вы доказываете владение доменом публикацией TXT-записи вида _acme-challenge.example.com со значением challenge-токена.
Плюсы DNS-01: не нужен доступ к веб-серверу, можно выпускать сертификаты для хостов, которые ещё не «живут» на этом сервере, и главное — поддержка wildcard. Минусы: нужно автоматизировать публикацию TXT, учитывать кеширование (TTL) и не попасть в ловушки split-horizon DNS.
Дальше — практический сценарий под админов: acme.sh как ACME-клиент, BIND9 с RFC2136 dynamic update через TSIG, и варианты, если у вас NSD (где RFC2136 нет).
Базовая схема выпуска wildcard через DNS-01
Процесс всегда одинаковый:
- acme.sh запрашивает сертификат для
*.example.com(обычно вместе сexample.comкак SAN). - ACME-сервер требует создать TXT на
_acme-challenge.example.com. - acme.sh добавляет TXT в авторитативный DNS (через RFC2136 или хук).
- acme.sh ждёт, пока запись станет видна снаружи, и подтверждает challenge.
- Сертификат выпущен: остаётся установить его и настроить обновление без простоя.
Самые частые проблемы: обновляете не тот сервер/не тот view, TXT виден только изнутри, или изменения не успевают разойтись по slave-серверам.

Подготовка DNS: TTL и split-horizon без сюрпризов
TTL для _acme-challenge
Если в зоне высокий TTL (например, 3600–86400), отладка и продления превращаются в ожидание и «фантомные» проверки. Практичный подход: задать для _acme-challenge TTL 60–300 секунд. Для самой зоны TTL можно оставить прежним.
Split-horizon DNS: почему «у меня видно, а Let’s Encrypt — нет»
Split-horizon — это когда домен резолвится по-разному из внутренней сети и из интернета. Для DNS-01 ключевое: TXT должен быть доступен публично с авторитативных NS, которые видит Let’s Encrypt.
Публикуйте TXT challenge именно в публичной DNS-плоскости. Внутренний view/зона могут быть отдельными, но challenge обязан попадать во внешнюю.
Если используете BIND9 с view, убедитесь, что динамическое обновление применяется к публичному view. Полезный разбор подходов и типовых ошибок — в статье про split-horizon views в BIND.
BIND9: RFC2136 dynamic update + TSIG (надёжно и безопасно)
BIND9 поддерживает RFC2136 dynamic update. Для DNS-01 это один из самых предсказуемых способов: acme.sh добавляет/удаляет TXT, BIND проверяет запрос TSIG-ключом, изменения появляются сразу, без ручного редактирования файлов зон.
1) Создаём TSIG-ключ для обновлений
На DNS-сервере (master), где находится зона example.com:
sudo tsig-keygen -a hmac-sha256 acme-example > /etc/bind/keys/acme-example.key
sudo chmod 640 /etc/bind/keys/acme-example.key
sudo chown root:bind /etc/bind/keys/acme-example.key
В файле будет блок key и секрет. Секрет нужен и BIND, и acme.sh на хосте, который выполняет выпуск.
2) Ограничиваем права: только TXT и только _acme-challenge
Минимально безопасный вариант — разрешить изменения только для конкретного имени и типа через update-policy:
include "/etc/bind/keys/acme-example.key";
zone "example.com" {
type master;
file "/var/lib/bind/db.example.com";
update-policy {
grant acme-example name _acme-challenge.example.com. txt;
};
};
- Имя в политике указывайте как FQDN с точкой на конце.
- Не выдавайте ключу права на всю зону без необходимости.
- Файл зоны держите там, где BIND может писать сам (часто
/var/lib/bind), иначе динамика будет «успешной», но фактически не сохранится.
3) Проверяем руками: nsupdate + dig
Перед автоматизацией стоит проверить всё вручную.
Добавляем тестовую TXT:
nsupdate -k /etc/bind/keys/acme-example.key << 'EOF'
server 127.0.0.1
zone example.com
update add _acme-challenge.example.com 60 TXT "test-token"
send
EOF
Проверяем на авторитативном сервере:
dig @127.0.0.1 _acme-challenge.example.com TXT +short
И отдельно проверяем публичную видимость, обращаясь к вашим авторитативным NS напрямую:
dig @ns1.example.com _acme-challenge.example.com TXT +short
Удаляем тест:
nsupdate -k /etc/bind/keys/acme-example.key << 'EOF'
server 127.0.0.1
zone example.com
update delete _acme-challenge.example.com TXT
send
EOF
4) Подключаем acme.sh по RFC2136 (dns_nsupdate)
acme.sh умеет RFC2136. Задаём параметры через переменные окружения:
export NSUPDATE_SERVER="ns1.example.com"
export NSUPDATE_KEY="acme-example"
export NSUPDATE_KEY_ALG="hmac-sha256"
export NSUPDATE_KEY_SECRET="BASE64SECRET=="
Выпускаем сертификат сразу на голый домен и wildcard:
acme.sh --issue --dns dns_nsupdate -d example.com -d '*.example.com'
Если у вас split-horizon и ns1 внутри/снаружи разный, в NSUPDATE_SERVER указывайте endpoint, который обновляет именно публичную зону.
Split-horizon в BIND9: как не обновлять «не тот» view
Когда зона определена в нескольких view, запрос dynamic update попадёт в тот view, куда BIND отнесёт клиента по match-clients. Из-за этого acme.sh может успешно обновлять внутренний view, а Let’s Encrypt будет проверять внешний и получать NXDOMAIN.
Практика, которая уменьшает число неожиданностей:
- Сделайте отдельный адрес/интерфейс для обновлений публичной зоны и привяжите его к публичному view (через
listen-onв нужном view). - В
match-clientsпубличного view явно разрешите IP хоста, где запускается acme.sh. - Во внутреннем view обновления либо запретите, либо используйте другой ключ, чтобы не перепутать среды.
Для диагностики смотрите логи named по событиям update и сравнивайте ответы dig @внутренний-IP и dig @внешний-IP для _acme-challenge.
NSD и DNS-01: почему «TSIG как в BIND» не работает и что делать
NSD — быстрый авторитативный сервер, но он принципиально не поддерживает RFC2136 dynamic update. Поэтому сценарий «acme.sh добавил TXT через nsupdate/TSIG прямо в NSD» в чистом виде не реализуется.
Рабочие схемы под DNS-01 обычно такие:
- NSD как slave, а master — BIND9 или Knot: acme.sh обновляет master по TSIG, NSD подтягивает изменения по AXFR/IXFR.
- Делегирование
_acme-challenge.example.comна отдельный DNS, где есть RFC2136 или API. - Хук + перезагрузка: скрипт правит include-файл/отдельную зону и вызывает
nsd-control reload(работает, но требует аккуратной синхронизации и контроля прав).
Вариант A (предпочтительный): BIND9 master + NSD slave
Публичные NS могут быть NSD, но источник зоны — master. Тогда acme.sh обновляет master по TSIG, а NSD быстро подхватывает изменения через transfer.
Пример на master (BIND9):
include "/etc/bind/keys/acme-example.key";
include "/etc/bind/keys/xfr-nsd.key";
zone "example.com" {
type master;
file "/var/lib/bind/db.example.com";
update-policy {
grant acme-example name _acme-challenge.example.com. txt;
};
allow-transfer {
key xfr-nsd;
};
};
На NSD настраиваете slave-зону, master-адрес и TSIG для XFR. Синтаксис зависит от версии NSD, но суть одна: принимать transfer только от master и (по возможности) только с ключом.
Вариант B: делегировать _acme-challenge на отдельный DNS
Если не хотите менять архитектуру зоны, делегируйте поддомен _acme-challenge на отдельный DNS, который умеет динамику:
_acme-challenge 60 IN NS ns-acme1.example.net.
_acme-challenge 60 IN NS ns-acme2.example.net.
Дальше поднимаете отдельную зону _acme-challenge.example.com на выбранном DNS и обновляете её через RFC2136/TSIG. Плюс в том, что split-horizon основной зоны не мешает: challenge живёт отдельно и публично.

Zero downtime renew: обновление сертификатов без разрыва соединений
Чтобы продление проходило без простоя, важны три вещи: стабильные пути файлов, «мягкая» перезагрузка сервиса и запуск хука только при реальном обновлении.
- Кладите ключ и fullchain в фиксированные пути, которые использует веб-сервер.
- Для Nginx обычно достаточно
reload, он перечитает сертификаты без обрыва активных соединений. - Следите, чтобы пользователь, под которым работает acme.sh (cron), имел права писать файлы и выполнять reload-команду.
Пример установки сертификата и команды перезагрузки для Nginx:
acme.sh --install-cert -d example.com --key-file /etc/ssl/private/example.com.key --fullchain-file /etc/ssl/certs/example.com.fullchain.pem --reloadcmd "systemctl reload nginx"
Важно: домен в --install-cert должен совпадать с тем, под которым acme.sh хранит конфигурацию выпуска. Если вы выпускали сразу example.com и *.example.com, чаще всего это базовый домен example.com.
Частые ошибки и быстрая диагностика
TXT появился «у меня», но Let’s Encrypt его не видит
- Проверяйте не резолвер, а авторитативные NS:
dig @ns1.example.com _acme-challenge.example.com TXT. - При split-horizon убедитесь, что обновляете публичный view.
- Если есть slave-серверы, проверьте, что они подтянули новую serial (и что transfer разрешён).
TSIG не проходит
- Сверьте
NSUPDATE_KEY,NSUPDATE_KEY_ALGи секрет. - Проверьте алгоритм: часто путают
hmac-sha256иhmac-sha512. - Имя ключа в BIND и у клиента должно совпадать посимвольно.
Параллельные выпуски ломают TXT
ACME может создавать несколько TXT для одного имени, если вы одновременно выпускаете несколько сертификатов. Убедитесь, что ваш механизм добавляет отдельные значения в RRset, а не перезатирает его одним значением. В связке acme.sh + RFC2136 обычно всё корректно; проблемы чаще появляются в самописных хуках.
Кеширование мешает тестам
Всегда отличайте «авторитативный ответ» от ответа рекурсивного резолвера. Для разбора используйте запросы к авторитативным серверам и при необходимости dig +trace. Низкий TTL именно для _acme-challenge заметно упрощает жизнь.
Чек-лист перед включением автопродления
- Публичные авторитативные NS отдают TXT
_acme-challengeнаружу. - TTL для challenge — 60–300 секунд или другой осмысленный минимум.
- TSIG-ключ ограничен до конкретного имени и типа TXT через
update-policy. - Если участвует NSD: либо он slave и получает IXFR/AXFR, либо
_acme-challengeделегирован отдельно. - acme.sh запускается по расписанию и имеет доступ к секретам и командам reload.
- Вы проверили ручной прогон продления (без ожидания срока) и посмотрели логи.
Итоги
Для wildcard Let’s Encrypt через DNS-01 наиболее чистая схема — авторитативный DNS с RFC2136 и аккуратно ограниченный TSIG-ключ, который умеет менять только _acme-challenge. В BIND9 это делается прозрачно через update-policy, а acme.sh хорошо автоматизирует выпуск и продление.
Если у вас NSD, прямые dynamic updates недоступны, но задача всё равно решается: используйте master (BIND9/Knot) + NSD как slave или делегируйте _acme-challenge на отдельную зону. В результате вы получаете предсказуемый DNS update и renew без ручных правок и без простоя.


