MAATRIX / Блог / DKIM подписывал письма ключом, которого уже не было в DNS

DKIM подписывал письма ключом, которого уже не было в DNS

MAATRIX

Почта уходила без единой ошибки в логах, Postfix отчитывался «250 2.0.0 Ok», а получатели у трети клиентов видели письма в спаме или не видели вовсе. Хуже всего то, что подпись DKIM на письме была технически валидной по формату — просто проверить её было нечем: соответствующей записи в DNS уже не существовало. Разбираем, как разошлись между собой то, что реально подписывает письма, и то, что видит внешний резолвер.

Что сломалось на самом деле

Компания держит собственную почтовую инфраструктуру: транзакционные письма (сброс пароля, чеки, уведомления) уходят с VPS через Postfix с милтером OpenDKIM, домен подписывается селектором mail1mail1._domainkey.example.com. Схема рабочая и не менявшаяся два года.

Примерно через три недели после планового переезда DNS-зоны на свой PowerDNS (домен раньше обслуживал сторонний регистратор, зону перенесли, чтобы не зависеть от чужой панели) в поддержку начали приходить однотипные жалобы: письма с подтверждением заказа то попадают в «Спам» в Gmail, то вовсе не доставляются в корпоративные почтовые ящики на Exchange Online. Доля таких жалоб росла медленно — не взрывом, а на 3-5% в день, из-за чего связать их с миграцией DNS трёхнедельной давности сразу не получилось: слишком большой разрыв во времени, чтобы держать это в голове как подозреваемое событие.

Важная деталь: со стороны отправителя всё выглядело идеально. Postfix отдавал письмо, mail-лог показывал успешную доставку до MX получателя, ни один алерт не срабатывал. Проблема жила целиком на стороне получателя — там, где письмо пытались провалидировать по DKIM и не могли найти публичный ключ.

Что видели в логах и метриках

Первым делом смотрели туда, куда обычно смотрят: mainlog Postfix и лог OpenDKIM на сервере-отправителе.

Aug 24 09:12:03 mail postfix/smtp[18231]: 4XyQ2b0Z0nz1abcd: to=<user@example-client.com>,
  relay=mx.example-client.com[203.0.113.10]:25, delay=0.87, status=sent (250 2.0.0 OK)
Aug 24 09:12:03 mail opendkim[1142]: 4XyQ2b0Z0nz1abcd: DKIM-Signature field added (s=mail1, d=example.com)

Всё чисто. Ни ошибок милтера, ни таймаутов, ни ретраев. Это первый сигнал, что причина не в отправителе — иначе OpenDKIM бы жаловался на отсутствие приватного ключа или проблему с файлом KeyTable.

Дальше открыли исходники писем на стороне получателя (в Gmail — «Показать оригинал») и увидели главное:

Authentication-Results: mx.google.com;
       dkim=neutral (no key for signature) header.i=@example.com header.s=mail1 header.b=Hs82nQi1;
       spf=pass (google.com: domain of noreply@example.com designates 198.51.100.20 as permitted sender) smtp.mailfrom=example.com;
       dmarc=fail (p=QUARANTINE sp=QUARANTINE dis=none) header.from=example.com

dkim=neutral (no key for signature) — это не «подпись неверна», это «нечем было проверить». SPF при этом проходил, а DMARC требовал строгого alignment хотя бы по одному из механизмов и с падением DKIM тоже уходил в fail, из-за чего письма квалифицировались как quarantine у части провайдеров.

Параллельно подняли агрегированные DMARC-отчёты (rua), которые домен получает от Google и Microsoft, и увидели наглядный скачок:

<record>
  <row>
    <source_ip>198.51.100.20</source_ip>
    <count>1240</count>
    <policy_evaluated>
      <disposition>quarantine</disposition>
      <dkim>fail</dkim>
      <spf>pass</spf>
    </policy_evaluated>
  </row>
</record>

С одного и того же IP, с которого письма всегда уходили без вопросов, DKIM внезапно стал массово падать. Это подтвердило: проблема системная, а не разовый сбой у конкретного получателя.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотезы, которые отбросили

Прежде чем дойти до правильного объяснения, проверили четыре версии, каждая на первый взгляд правдоподобная.

  • Репутация IP. Проверили адрес отправителя по нескольким чёрным спискам (Spamhaus, Barracuda) — чисто. Отбросили: репутация не объясняет именно dkim=neutral, она давала бы spam без деталей по DKIM.
  • Сломался SPF. SPF в заголовках стабильно проходил (spf=pass), запись TXT для SPF проверили dig-ом отдельно — она была на месте и не менялась. Отбросили.
  • Кто-то из разработчиков переиздал DKIM-ключ. Проверили дату модификации файлов в /etc/opendkim/keys/ — последнее изменение полгода назад, задолго до миграции DNS. Приватный ключ на сервере не трогали. Отбросили.
  • Проблема в самом OpenDKIM после обновления пакета. На сервере действительно недавно обновляли пакеты (apt upgrade), заподозрили баг в новой версии opendkim. Собрали тестовое письмо, руками проверили подпись локальной утилитой opendkim-testkey — она как раз и подсказала настоящее направление поиска (см. ниже), так что эта гипотеза не столько отброшена, сколько привела к правильному ответу.

Как нашли реальную причину

opendkim-testkey — маленькая утилита, которая проверяет ровно то же самое, что делает получатель: берёт селектор и домен из KeyTable, идёт в DNS и сверяет публичный ключ с приватным на диске.

$ opendkim-testkey -d example.com -s mail1 -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key 'mail1._domainkey.example.com'
opendkim-testkey: 'mail1._domainkey.example.com' record not found

Запись не найдена. Проверили напрямую через внешний резолвер, а не через локальный кэш сервера:

$ dig +short TXT mail1._domainkey.example.com @8.8.8.8
$ dig +short TXT mail1._domainkey.example.com @1.1.1.1

Оба запроса вернули пустой ответ. При этом внутри офиса, где резолвер иногда ходил в старый кэш и в тестовую зону на бывшем DNS-провайдере, запись как будто «была видна» — отсюда и обманчивое ощущение, что с DNS всё в порядке, если проверять по привычке из офисной сети.

Дальше подняли changelog миграции DNS-зоны на PowerDNS. Экспорт зоны со старого провайдера делали через его же встроенный экспорт в формат BIND, а перед импортом кто-то из команды вручную проверял выгруженный zone-файл на «мусорные» записи — и в этом файле построчно сравнивали в первую очередь A, MX, CNAME и SPF/DMARC TXT-записи, потому что список проверки был составлен по памяти, а не по реальному содержимому зоны. Запись mail1._domainkey в этот список не попала: она длинная, лежит на отдельном поддомене _domainkey, визуально теряется среди служебных записей _dmarc, _acme-challenge и SRV-записей для другого сервиса. Её просто не заметили и не перенесли.

То есть ключ на сервере был на месте, OpenDKIM честно им подписывал каждое письмо, а мир снаружи уже три недели как не мог найти этой записи в DNS вообще — потому что она физически не существовала в новой зоне.

Почему сбой проявился не сразу и не у всех

Здесь стоит объяснить, почему падение доставляемости растянулось на дни, а не случилось разом в момент переключения NS.

Во-первых, часть провайдеров и почтовых серверов кэширует DNS-ответы, в том числе отрицательные (NXDOMAIN/пустой ответ), на срок, заданный TTL записи или на дефолтное значение резолвера — какое-то время получатели ещё пользовались старым, валидным результатом проверки. Пока кэш не истёк у всех подряд, часть писем проходила DKIM нормально.

Во-вторых, DMARC-политика домена стояла в p=quarantine, а не p=reject, и с относительно щадящим pct — то есть даже при падении DKIM часть писем всё равно доставлялась, просто в спам, а не отбивалась совсем. Из-за этого симптом выглядел как «иногда попадает в спам», а не как «письма перестали приходить», что и затянуло диагностику: разработчики искали проблему в контенте письма и репутации, а не в DNS.

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

Что изменили после

По итогам разбора сделали три изменения, все — на стороне процесса, а не только техники.

  1. Проверка DKIM снаружи, а не изнутри. Добавили в мониторинг регулярный (раз в час) внешний запрос dig TXT по каждому активному селектору через публичные резолверы (не через локальный кэш сервера), с алертом при пустом ответе или NXDOMAIN. Раньше такой проверки не было вообще — верили, что раз ключ на диске есть, то и в DNS он есть.
  1. Чек-лист миграции DNS-зоны по типам записей, а не «на глаз». Теперь перед переключением делегирования на новый DNS-провайдер обязателен построчный diff всех TXT-записей поддомена _domainkey, _dmarc и SPF между старой и новой зоной — не выборочная визуальная сверка, а автоматическое сравнение файлов (diff по отсортированным зонам) с ручным подтверждением каждой записи из списка.
  1. Более длинное окно перекрытия при смене NS. Делегирование на новых серверах включают заранее, при этом старые NS ещё какое-то время продолжают отвечать той же зоной — это даёт запас на случай, если что-то забыли перенести, и не превращает пропущенную запись в мгновенный сбой у всех получателей сразу.

Отдельно завели себе привычку: раз в квартал прогонять opendkim-testkey по всем активным селекторам вручную, даже если ничего не менялось — это дешевле, чем разбор жалоб через три недели после тихой поломки.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро проверить, что DKIM-запись реально есть в DNS, а не только у меня в кэше?

Выполните dig +short TXT <селектор>._domainkey.<домен> @8.8.8.8 с явным указанием внешнего резолвера — так вы обходите локальный DNS-кэш сервера и видите то же, что видит получатель.

Почему dkim=neutral (no key for signature), а не явная ошибка?

Так работает протокол: отсутствие ключа в DNS — не то же самое, что неверная подпись. Проверяющая сторона трактует это мягче, чем явный fail, но при активном DMARC с проверкой alignment по DKIM результат всё равно может привести к quarantine или reject.

Помогло бы это предотвратить, если бы DMARC стоял на p=reject?

Нет — reject только ужесточил бы последствия (письма отбивались бы полностью, а не уходили в спам), но не предотвратил бы саму причину. От самого разрыва между сервером и DNS защищает только независимый внешний мониторинг записей, а не политика DMARC.

Нужно ли для этого держать собственный DNS-сервер, например PowerDNS, на отдельном VPS?

Не обязательно, но если почта и DNS размещены на разных площадках или у разных провайдеров, риск подобного разъезда данных при миграции только растёт — проверка синхронизации между тем, что подписывает сервер, и тем, что видно в зоне, нужна в любом случае.

Как отличить такую проблему от бана IP или репутационной блокировки?

По заголовку Authentication-Results в исходнике письма у получателя: репутационная блокировка обычно не трогает dkim=, она либо отклоняет соединение на уровне SMTP, либо помечает письмо отдельным признаком спам-фильтра, не связанным с самой DKIM-подписью.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →