Привет, Хабр!
Я в одиночку делаю VantageDNS, рекурсивный DNS-резолвер с фильтрацией. На днях от нечего делать прогнал свой прод через публичный тест DNS-OARC. Рандомизация портов зелёная, рандомизация DNS ID зелёная, TCP, IPv6, QNAME-минимизация зелёные. И одна красная строка: Lookup succeeded while signature was invalid.
Перевожу: мой резолвер отдавал клиентам ответы с невалидными DNSSEC-подписями и делал вид, что всё хорошо. Полгода. При этом мониторинг был зелёный все эти полгода, и он не врал.
Ниже про то, почему выключенная валидация это самый неудобный класс багов, как я её включал и уронил резолвер в crash-loop с первой попытки, и как проверять результат, чтобы не обмануть себя.
Одна красная строка среди зелёных
Тест DNS-OARC (cmdns) гоняет через ваш резолвер несколько десятков запросов с рандомными именами в своей зоне и смотрит, что происходит по дороге. Он проверяет энтропию source-портов, энтропию DNS ID, умеет ли резолвер в TCP и IPv6, минимизирует ли QNAME, и отдельно проверяет DNSSEC.
Мой результат выглядел так:
Port Number Randomization Success stdev 12137.83, bits 15.36DNS ID Randomization Success stdev 17760.15, bits 15.91TCP SuccessIPv6 SuccessQNAME Minimisation SuccessInvalid DNSSEC Signature Failure Lookup succeeded while signature was invalid
Пять из шести зелёные, и это даже приятно: над рандомизацией и QNAME-минимизацией я работал осознанно. А вот шестая строка означала, что одного свойства у меня нет вообще.
Проверяю руками на эталонном домене, у которого подпись сломана специально:
$ dig @127.0.0.1 -p 5353 dnssec-failed.org A +noall +comments ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16345 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0
NOERROR и один ответ там, где обязан быть SERVFAIL. То есть если кто-то подсунет мне подделанные данные с битой подписью, я их приму и отдам пользователю. Ровно от этого DNSSEC и придуман.
Почему это не поймал мониторинг
Тут интересный момент, ради которого я и пишу статью.
У меня есть health-check каждые 60 секунд, есть probe-агент, есть алерты на degraded и down, есть метрики latency и cache hit rate. Всё это отлично ловит падения. Ничего из этого не могло поймать отсутствие валидации, потому что отсутствие валидации не выглядит как поломка.
Когда валидация выключена, резолвер отвечает быстрее (не нужно тянуть DNSKEY и DS, не нужно считать криптографию), отдаёт больше успешных ответов (домены со сломанным DNSSEC у вас резолвятся, а у соседа нет) и вообще ведёт себя как здоровый. Все ваши SLI зелёные. Метрика «доля ответов, которые мы обязаны были отклонить, но не отклонили» в дашбордах не заводится, потому что её некому посчитать: вы же не валидируете.
Это класс багов «отсутствующее свойство». Мониторинг устроен вокруг событий: что-то упало, что-то замедлилось, счётчик ошибок вырос. А тут ничего не происходит. Свойства просто нет, и его отсутствие проявляется единственным способом: когда кто-то целенаправленно приносит вам плохую подпись и смотрит, проглотите ли вы её.
Отсюда практический вывод, который я себе записал: свойства безопасности нужно проверять активно и регулярно, тем же способом, каким их проверяет внешний тест. Не «алерт, если сломается», а «раз в сутки спросить dnssec-failed.org и убедиться, что пришёл SERVFAIL».
Комментарий, который пережил спринт
Дальше я полез в конфиг unbound и нашёл там причину. Свою собственную, полугодовой давности:
# DNSSEC validation — отключено в MVP, добавим в Sprint 5 # (требует auto-trust-anchor-file + root.key, которого нет в образе # по умолчанию). Параметры hardening оставлены, они работают и без validation. harden-glue: yes harden-below-nxdomain: yes harden-referral-path: yes
Sprint 5 прошёл. Прошли шестой, седьмой и всё, что было дальше. Комментарий остался.
Отдельная ирония в том, что у меня в блоге есть статья про то, как я писал DNSSEC-валидатор на Go для edge-бинаря. Валидатор существует. Только рекурсию в проде делает не он, а unbound-сайдкар, к которому edge форвардит запросы. Мой Go-код валидирует то, что делает сам, а реальный трафик шёл мимо, через компонент с выключенной проверкой. Красиво написанный модуль не спасает, если продовый путь запроса идёт другой дорогой.
Мораль про техдолг звучит банально, но конкретный механизм полезный: TODO в конфиге не имеет срока и не имеет владельца, поэтому живёт вечно. Если бы я в мае завёл задачу с датой вместо комментария в yaml, полугода бы не случилось.
Первая попытка включить и crash-loop
Включение выглядит на две строки. Берём trust anchor корня, говорим unbound валидировать:
module-config: «validator iterator» auto-trust-anchor-file: «/opt/unbound/etc/unbound/root.key»
Сам якорь генерируется штатной утилитой:
$ unbound-anchor -a /opt/vantagedns-edge/root.key $ grep -c ‘DNSKEY’ root.key 2
Два KSK корня, всё как надо. Перезапускаю и получаю:
[1788131039] unbound[1:0] fatal error: could not open autotrust file for writing, /root.key.1-0-561a2f353970: Permission denied
Контейнер уходит в перезапуск по кругу. Разбираемся.
Опция auto-trust-anchor-file это не «файл, откуда читать якорь». Это RFC 5011, то есть механизм, при котором резолвер сам отслеживает ротацию корневого ключа и переписывает файл. Чтобы переписать атомарно, unbound создаёт временный файл рядом с якорём и потом переименовывает. Значит, ему нужна запись в каталог, а не только в файл.
Дальше два обстоятельства образа. Первое: unbound работает в chroot, поэтому путь /opt/unbound/etc/unbound/root.key внутри превращается в /root.key, и сообщение об ошибке выглядит пугающе, будто он лезет в корень файловой системы. Второе: процесс работает под непривилегированным пользователем _unbound, а каталог с конфигами принадлежит root. Плюс я смонтировал ровно один файл, а не каталог, так что создать соседний временный файл невозможно в принципе.
Симптом при этом максимально неприятный: резолвер не стартует вообще. Не «работает без валидации», а падает при старте. Хорошо, что в этот момент edge умеет фоллбечиться на публичные апстримы, и пользователи ничего не заметили.
Статический якорь и чем за него платим
Правильных выходов два. Можно дать unbound writable-каталог под якорь и оставить RFC 5011. Можно перейти на статический якорь и обновлять его самому:
module-config: «validator iterator» trust-anchor-file: «/opt/unbound/etc/unbound/root.key»
Я выбрал второе. Причина простая: writable-каталог внутри контейнера, который в остальном read-only, это ослабление ради события, которое случается раз в несколько лет. Корневой KSK менялся в последний раз в 2018 году, до этого он вообще не менялся с запуска DNSSEC в корне.
Цена честная и я её называю: теперь при ротации корневого ключа мой резолвер сломается, если я не обновлю файл заранее. Процедура на две команды:
$ unbound-anchor -a /opt/vantagedns-edge/root.key $ docker compose up -d unbound
Плюс напоминание в календаре и подписка на анонсы IANA. Если у вас инфраструктура больше одной ноды, скорее всего вам выгоднее сделать writable-каталог и не думать об этом. У меня одна нода, и я предпочёл явную ручную операцию неявному праву на запись.
Как проверять, чтобы не обмануть себя
Тут я чуть не наступил на вторую граблю, уже психологическую.
Сразу после рестарта unbound в логах пошли SERVFAIL на вполне живых доменах: static.xx.fbcdn.net, gql.twitch.tv, sync-v2.brave.com. Первая мысль была «валидация ломает половину интернета, надо откатывать». Проверил через минуту, уже прогретым кэшем, и сравнил с публичными резолверами:
static.xx.fbcdn.net мы:NOERROR 1.1.1.1:NOERROR 8.8.8.8:NOERROR gql.twitch.tv мы:NOERROR 1.1.1.1:NOERROR 8.8.8.8:NOERROR sync-v2.brave.com мы:NOERROR 1.1.1.1:NOERROR 8.8.8.8:NOERROR
Это был холодный кэш. Резолвер, который только что стартовал, идёт в корень за всем подряд, часть запросов таймаутится, в логи сыпется misc failure. Судить о резолвере по первым шестидесяти секундам после рестарта нельзя.
Минимальный набор проверок, который я теперь считаю обязательным:
Домены со специально сломанной подписью должны давать SERVFAIL, и не один раз, а стабильно:
dnssec-failed.org SERVFAIL SERVFAIL SERVFAIL sigfail.verteiltesysteme.net SERVFAIL SERVFAIL SERVFAIL
Подписанные домены должны приходить с флагом ad (authenticated data). Если флага нет, значит валидация не сработала, даже когда ответ верный:
$ dig @127.0.0.1 -p 5353 cloudflare.com A +noall +comments ;; flags: qr rd ra ad
Обычные популярные домены должны продолжать резолвиться, и сравнивать их надо не с ожиданиями, а с другим валидирующим резолвером. 1.1.1.1 и 8.8.8.8 оба валидируют, и если они отвечают так же, как вы, вопрос закрыт.
И главное: всё это должно ходить периодически, а не один раз при настройке. Я добавил проверку dnssec-failed.org в свой регулярный чек, потому что единственный способ узнать, что валидация отвалилась, это принести резолверу плохую подпись и посмотреть на реакцию.
Сколько это стоит
Главный аргумент против включения валидации обычно звучит как «станет медленнее»: резолверу нужно дотянуть DNSKEY и DS по всей цепочке и посчитать криптографию. Я померил у себя.
Гоняю случайные несуществующие поддомены, чтобы гарантированно мимо кэша, отдельно по подписанным зонам и по неподписанным:
подписанные зоны (ietf.org, nic.cz): 24 ms среднее по 16 замерам неподписанные (github.com, twitch.tv): 28 ms среднее по 16 замерам из кэша: 0 ms
Разницы нет. Точнее, она есть, но в пользу подписанных зон, что означает только одно: на холодном запросе всё определяют скорость и география authoritative-серверов, а не наличие подписи. Криптография на фоне сетевых RTT не видна.
Оговорка обязательна: это грубый замер на одной ноде, разные зоны обслуживаются разными операторами, выборка маленькая. Он не доказывает, что валидация бесплатна вообще, он доказывает, что в моём проде она не попала в бюджет latency. Память тоже не выросла заметно: unbound держится в районе 25 МБ при лимите 192.
Реальная цена оказалась не в производительности, а в операционке. Теперь у меня есть файл, который надо обновлять руками, и класс доменов, которые перестанут открываться у пользователей, если их владелец накосячит с подписью. Второе это не баг, а ровно то, за чем валидацию включают, но support-тикет вы получите именно про него.
Чего DNSSEC не даёт
Раз уж включил, стоит сказать и про границы, чтобы не выглядело как продажа серебряной пули.
DNSSEC подтверждает, что данные пришли из зоны, которая их подписала, и что их не поменяли по дороге до валидирующего резолвера. Он не шифрует запросы: наблюдатель между вами и резолвером видит всё то же самое, для этого нужны DoH, DoT или DoQ, и они решают ортогональную задачу. Он не защищает последнюю милю: если валидация происходит на резолвере, а клиент ходит к нему по обычному UDP, подменить можно уже на этом участке.
И самое отрезвляющее: большая часть доменов, которые открывают ваши пользователи, вообще не подписана. Для неподписанной зоны валидация не даёт ничего, кроме подтверждения, что зона действительно не подписана. То есть выигрыш от включения не «интернет стал безопасным», а «конкретный класс атак с подменой ответа для подписанных зон перестал работать».
Флаг ad при этом почти никто из клиентов не проверяет. Браузер, который ходит к вам по DoH, обычно доверяет резолверу целиком. Это значит, что ответственность за валидацию лежит на операторе резолвера, и пользователь не может её проконтролировать. Что, собственно, и делает мой полугодовой прокол неприятным: никто из моих пользователей не мог бы это заметить, даже если бы захотел.
Что я вынес
Свойства безопасности не мониторятся сами. Падение сервиса вы увидите за минуту, а отсутствие валидации может жить годами, потому что снаружи выглядит как здоровый сервис, отвечающий чуть быстрее соседей. Если свойство нельзя проверить активной пробой, считайте, что его нет.
Дальше конкретика, которую можно унести к себе. Прогоните свой резолвер через cmdns.dns-oarc.net, это пять минут и никакой регистрации. Проверьте dnssec-failed.org: если приходит NOERROR, валидации у вас нет. Если включаете в контейнере, помните, что auto-trust-anchor-file требует права на запись в каталог, а не в файл, и в chroot путь в сообщении об ошибке будет выглядеть иначе, чем в конфиге. И не оценивайте резолвер по первой минуте после рестарта.
Ну и про комментарии в конфигах. Фраза «добавим в Sprint 5» в yaml это не план, это оставленная себе записка, которую никто не прочитает. Задача с датой в трекере работает, комментарий рядом с выключенной опцией не работает вообще.
-
vantagedns.com/press-kit — для авторов Pro-аккаунт на 90 дней
Если вы держите свой резолвер, потратьте пять минут на тест DNS-OARC прямо сейчас и напишите в комментариях, что он вам показал. Мне особенно интересны те, у кого валидация включена, но ad не приходит: у меня есть подозрение, что это чаще, чем кажется.
ссылка на оригинал статьи https://habr.com/ru/articles/1076494/