Маркетинг попросил вернуть старую промо-страницу - ту, что делали к акции три года назад. Открываю страницу по её адресу, чтобы посмотреть, что от неё осталось, и вижу чужой сайт. Домен наш, имя в адресной строке наше, замочек на месте, содержимое - не имеет к нам никакого отношения.

Страницу тогда выключили, а запись в DNS осталась. За три года она пережила и акцию, и сам сервис, на который указывала, - и в какой-то момент начала отправлять посетителей на чужой сайт. Такие записи называют висячими, и в зоне любой компании, которая живёт дольше пары лет, их обычно несколько десятков.

Как получается висячая запись

Разберём тот же случай по шагам. Маркетингу нужна промо-страница, и её кладут не к себе, а на внешний хостинг: статические страницы в объектном хранилище, GitHub Pages, конструктор сайтов. У всех у них одна модель - вы придумываете имя проекта, и хостинг отдаёт вам адрес вида имя-проекта.хостинг.com. Имя выдаётся по принципу «кто первый занял», как никнейм.

Чтобы страница открывалась на своём домене, в зоне компании заводят запись, которая перенаправляет на этот адрес:

promo    CNAME    company-promo-2023.<хостинг>.

Акция кончается. Проект на хостинге удаляют - счёт закрыт, задача закрыта. В зону никто не заглядывает, потому что DNS ведёт другая команда, а тикета на удаление никто не заводил.

Имя проекта company-promo-2023 на стороне хостинга освободилось. Оно снова доступно для регистрации - любому. А запись в вашей зоне продолжает всех туда отправлять.

Тот, кто зарегистрирует это имя, начнёт отдавать собственный контент с адреса promo.example.com. И это не домен, похожий на ваш, а ваш собственный: сертификат выпишется автоматически, в адресной строке будет ваше имя, замочек будет на месте.

Почему это не ловится обычным мониторингом

Мониторинг доступности проверяет сайты, которые вы знаете. Висячая запись - по определению та, про которую забыли, её нет ни в одном списке.

Проверка «а резолвится ли имя» тоже не помогает - и вот здесь главная ловушка. Возьмём заведомо несуществующий проект на GitHub Pages и запросим у DNS его адреса:

$ dig +short nonexistent-abc123.github.io
185.199.109.153
185.199.111.153
185.199.110.153
185.199.108.153

Четыре адреса, всё в порядке. Только проекта с таким именем не существует - я его выдумал.

Так происходит потому, что у хостинга в DNS стоит запись-джокер на *.github.io: она отвечает на любое имя в этой зоне, независимо от того, зарегистрировано оно или нет. DNS отвечает за имена, а не за содержимое, и сведений о том, занят ли проект, в нём нет.

Значит, проверка на уровне DNS даёт одинаковое «да» и для рабочего сервиса, и для пустого места. Разница появляется, только когда вы запросите по этому адресу саму страницу:

$ curl -s -o /dev/null -w "%{http_code}\n" http://nonexistent-abc123.github.io/
404
$ curl -s http://nonexistent-abc123.github.io/ | grep -o '<title>.*</title>'
<title>Site not found &middot; GitHub Pages</title>

То же самое у объектного хранилища, только ответ приходит в XML:

$ curl -s http://nosuchbucket-abc123.s3.amazonaws.com/
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message>
<BucketName>nosuchbucket-abc123</BucketName>...

Site not found и NoSuchBucket - это и есть признак свободного имени: хостинг прямо говорит, что проекта у него нет, а ваша запись в DNS продолжает на него указывать. У большинства крупных сервисов такая страница своя и узнаваемая; если её нет, придётся сравнивать ответ с ответом на заведомо выдуманное имя того же хостинга.

Оговорка: часть хостингов уже требует подтвердить владение доменом, прежде чем к проекту можно привязать свой поддомен, - там сценарий закрыт. Проблема остаётся у тех, где такой проверки нет, и у записей, заведённых до того, как её ввели.

Чем это оборачивается

Возражение здесь понятное: промо-страница трёхлетней давности никому не нужна, кому она навредит. Но ущерб не в странице, а в имени, под которым она теперь отдаётся.

Cookie домена. Если приложение выставляет cookie на .example.com - а так делают, чтобы одна авторизация работала на всех поддоменах, - то страница на promo.example.com эти cookie получит. Браузер отработает штатно: область действия задали вы сами, и он приложит cookie к запросу на любой поддомен внутри неё. Дальше их прочитает тот, кто этим поддоменом управляет. HttpOnly тут не помогает: читать cookie из JavaScript и не требуется, достаточно того, что браузер отправил их на сервер. Secure к этой ситуации отношения не имеет вовсе - он ограничивает только передачу по HTTPS. Помогает префикс __Host-: он привязывает cookie к конкретному хосту. Цена - та самая единая авторизация на всех поддоменах, ради которой cookie и ставили на весь домен; выбирать приходится между удобством и этим риском.

Доверие к домену. Ссылка на promo.example.com в письме проходит и почтовый шлюз, и глаза самого получателя: домен-то родной. Все советы вида «смотрите на адрес» здесь работают против защищающегося.

Списки разрешённых источников. Ваш собственный поддомен часто оказывается в Content-Security-Policy, в Access-Control-Allow-Origin, в правилах прокси, в списке разрешённых redirect_uri у OAuth-клиента. Все эти списки писались, когда поддомен был ваш.

Подтверждение владения. Часть сервисов принимает подтверждение по файлу на конкретном хосте - и если этот хост попал в чужие руки, подтверждение получит тот, кто им управляет.

Откуда берётся зона, которую никто не помнит

Причины повторяются от компании к компании, и ни одна не про халатность конкретного человека.

DNS-запись живёт дольше проекта, потому что её удаление никогда не входит в чек-лист закрытия. Открытие - входит: без записи ничего не заработает, про неё вспомнят обязательно.

Записи заводят несколько команд, а зона одна. Инфраструктура, разработка, маркетинг, внешнее агентство - у каждого свои поддомены, общего реестра нет ни у кого.

Автоматика создаёт записи под каждую ветку и окружение. Часть контуров при этом выведена из-под автоматики «потому что там что-то важное», и записи от них удаляют руками - то есть не удаляют.

Компанию купили вместе с её зоной. Что там было - не знает уже никто.

Как это чинится

Взять зону целиком. Не по памяти, а выгрузкой у DNS-провайдера через его API или панель. Если зон несколько - все, включая те, что на почтовом провайдере и у регистратора отдельно.

Разметить владельцев. Каждая запись получает две пометки: команду-владельца и причину существования. Форма годится любая - комментарий в описании записи у провайдера, отдельная таблица, поле в системе, откуда зона разворачивается кодом; важно, чтобы пометка жила рядом с записью и обновлялась вместе с ней. Всё, для чего владельца не нашлось, идёт в отдельный список - с ним и работаем.

Проверить, куда указывает каждая запись. Для CNAME - существует ли на той стороне проект с таким именем: смотреть надо на ответ приложения, а не на факт резолва. Для A и AAAA - принадлежит ли ещё адрес вам; облачные адреса возвращаются в общий пул и достаются следующему арендатору, и это обычная штатная механика, а не сбой.

Удалять, а не «оставить на всякий случай». С формулировки «вдруг понадобится» вся история и начинается заново. Понадобится - заведут заново, это одна запись.

Закрыть источник. Удаление записи вносится в процедуру вывода сервиса из эксплуатации, рядом с отзывом сертификата и закрытием доступов. Без этого через год список соберётся снова, и вы будете разбирать его с нуля.

Что посмотреть у себя

Выгрузить зону и посчитать записи. В моей практике расхождение с тем, что называют по памяти, получается в разы - этого одного обычно достаточно, чтобы разговор про инвентаризацию перестал быть теоретическим.

Отобрать все CNAME, ведущие на внешние домены. Это группа с наибольшим риском: имя проекта на той стороне может освободиться и достаться кому-то другому. Для каждого проверить ответ сервиса, а не факт резолва.

Посмотреть, на какой домен приложение выставляет cookie. Если на .example.com, а не на конкретный хост - цена висячей записи вырастает, и разбор списка становится срочным.

И отдельно - посмотреть логи Certificate Transparency по своему домену: проще всего через поиск на crt.sh по маске %.example.com или через API любого монитора CT. Там перечислены все имена, для которых кто-либо когда-либо выписывал сертификат, - включая те, что выписывали не вы. Часть этих имён вы не найдёте ни в одном своём списке.