Одна опечатка в SPF положила рассылку на неделю
В понедельник утром открытия писем маркетинговой рассылки просели почти до нуля, а через пару часов начали сыпаться жалобы, что не доходят даже транзакционные письма - подтверждения заказов, сброс пароля, уведомления. Никто не трогал почтовый сервер, никто не менял код отправки. Единственное, что произошло накануне вечером - добавили новый сервис рассылок и дописали одну строку в SPF-запись. Дальше неделя ушла на то, чтобы понять: сломалось не потому, что сервис плохой, а потому что в записи была опечатка в одном символе - и из-за неё DNS-проверка почты для всего домена перестала работать в принципе, а не только для нового отправителя.
Содержание
Что сломалось: рассылка перестала доходить в понедельник
Компания отправляет почту с одного домена сразу двумя каналами: транзакционные письма идут через собственный SMTP-релей на VPS, а маркетинговая рассылка - через внешний ESP (Email Service Provider). В воскресенье вечером решили подключить второй ESP для триггерных писем по брошенным корзинам. Чтобы почта нового сервиса не улетала в спам, инженер зашёл в панель DNS-провайдера и вручную дописал в существующую SPF-запись ещё один include: для домена нового провайдера.
Запись до правки выглядела примерно так:
v=spf1 include:_spf.google.com include:mailgun.org include:sendgrid.net -all
После правки в неё добавили ещё один include - и именно в новой строке допустили опечатку: вместо .com в конце домена авторизации оказалось .con. Визуально в текстовом поле панели DNS это незаметно, а автоматической проверки синтаксиса при сохранении TXT-записи ни один регистратор не делает - строка сохраняется как обычный текст.
Уже утром понедельника первые письма начали возвращаться с отказами. Проблема была не только у нового провайдера - "легли" вообще все письма с домена, включая транзакционные, которые шли через старый, давно работающий канал. Это ключевая деталь инцидента: SPF - это одна запись на весь домен, а не набор независимых правил по отправителям. Если запись становится синтаксически некорректной, страдает проверка для абсолютно любого сервера, который отправляет письма от имени домена, вне зависимости от того, при чём тут вообще новый include.
Что показывали логи и метрики
Первым делом посмотрели логи возвратов (bounce) на стороне обоих ESP и на собственном постфиксе. Картина была пёстрой и оттого сбивающей с толку:
- часть писем в Gmail получала мягкий бounce с текстом вида
550-5.7.26 This mail is unauthenticated; - часть в Outlook/Microsoft 365 отбивалась с
550 5.7.1 Unable to authenticate; - Mail.ru и Yandex часть писем не отклоняли, а тихо отправляли в папку "Спам".
Разные формулировки у разных провайдеров - это нормально, каждый принимающий сервер описывает нарушение аутентификации своими словами. Общая нить нашлась не в тексте бounce, а в отчётах DMARC (agregate reports, rua), которые домен собирал уже несколько месяцев. В XML-отчётах за понедельник почти все записи показывали не spf: fail, а spf: permerror. Это разные вещи: fail означает, что IP-адрес отправителя проверили и он не входит в разрешённый список; permerror означает, что сама SPF-запись не смогла быть корректно разобрана или один из её механизмов не удалось разрешить через DNS. То есть проблема была не в том, кто отправляет письма, а в том, что сама запись сломана. Именно на этом различии в итоге и нашлось решение - но не сразу, потому что в первый день на "permerror" внимание не обратили: отчётов было много, разбирать XML руками неудобно, а бounce-сообщения от ESP выглядели как обычная жалоба на репутацию. Про то, как вообще устроен путь письма до папки "Спам" и какие точки проверки на нём есть, у нас есть отдельный разбор - как письма попадают в спам.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГипотеза первая: превысили лимит в 10 DNS-lookup
Первая версия, за которую зацепилась команда - лимит SPF в 10 DNS-запросов на одну проверку (правило из RFC 7208). Каждый include, a, mx, exists, redirect в записи - это отдельный DNS-lookup, причём вложенные include считаются рекурсивно. За несколько месяцев в запись добавили провайдера транзакционной почты, два сервиса рассылок, CRM для писем от менеджеров и SPF-блок для внешнего антиспам-фильтра. Прибавление ещё одного include выглядело вполне правдоподобной причиной: если лимит и так был на грани, новая строка могла его превысить, а превышение лимита тоже приводит к permerror - тому самому, что было в отчётах.
Лимит посчитали вручную: раскрыли dig txt для домена и для каждого include внутри него, построили дерево вложенности. Быстрее это делать скриптом, а не в уме - вот примерный подход на Python с dnspython:
import dns.resolver
LOOKUP_MECHANISMS = ("include:", "a", "mx", "exists:", "redirect=")
def count_lookups(domain, seen=None):
seen = seen or set()
if domain in seen:
return 0
seen.add(domain)
total = 0
try:
answers = dns.resolver.resolve(domain, "TXT")
except Exception as e:
print(f"{domain}: DNS error -> {e}")
return 0
for rdata in answers:
txt = b"".join(rdata.strings).decode()
if not txt.startswith("v=spf1"):
continue
for token in txt.split():
if token.startswith("include:"):
total += 1
total += count_lookups(token.split(":", 1)[1], seen)
return total
print(count_lookups("example.com"))
Результат разочаровал: с учётом всех вложенных include получилось девять запросов, а не десять и не одиннадцать - лимит не был превышен ни до правки, ни после. Гипотезу отбросили в тот же день, но время на неё ушло: пока считали руками и перепроверяли, сутки уже прошли, а рассылка продолжала лежать.
Гипотеза вторая: TTL и медленное распространение записи
Вторая версия - классическая для любого, кто когда-то обжигался на DNS: может быть, запись обновилась не везде, и разные резолверы видят разные версии SPF. У домена TTL на TXT-записи был выставлен на 86400 секунд (сутки) - остаток от старых настроек, когда записи меняли редко. Логика была такая: раз TTL высокий, часть резолверов ещё могла кэшировать старую, рабочую версию записи, а другая часть - уже новую, сломанную, из-за чего поведение получалось "рваным" - то доходит, то нет.
Проверили с нескольких публичных резолверов сразу:
dig +short TXT example.com @8.8.8.8
dig +short TXT example.com @1.1.1.1
dig +short TXT example.com @9.9.9.9
dig +short TXT example.com
Все четыре запроса вернули одну и ту же, уже новую версию записи - без расхождений. Это логично: TXT-запись домена меняется на авторитетных серверах имён провайдера DNS, и после публикации она сразу отдаётся всем, кто идёт за ней впервые; TTL влияет только на то, как долго резолвер, уже закэшировавший старое значение, будет отдавать его повторно, не переспрашивая. К моменту проверки прошло уже больше суток - весь возможный кэш с TTL 86400 успел устареть в любом случае. А главное: если бы дело было в частичном распространении, проблема постепенно сходила бы на нет сама по себе в течение одного дня. Она не сходила - шёл уже третий день, а картина не менялась. Гипотезу закрыли, TTL на будущее всё равно снизили до разумных значений, но причину нужно было искать в другом месте.
Как нашли реальную причину: опечатка в одном include
К середине недели решили не гадать, а проверить SPF-запись механизм за механизмом - буквально выполнить dig для каждого домена, упомянутого в include, по отдельности:
dig +short TXT _spf.google.com
dig +short TXT mailgun.org
dig +short TXT sendgrid.net
dig +short TXT sendgrid.con
Последний запрос не вернул ничего - домен sendgrid.con не существует (NXDOMAIN). Опечатка нашлась не там, где её ждали: не в новом провайдере из воскресной правки, а в соседнем include, который вообще не трогали при последнем изменении - судя по всему, эта опечатка попала в запись ещё раньше, при одном из более старых редактирований, и до поры до времени ничего не ломала.
Здесь и кроется главный технический нюанс, из-за которого инцидент выглядел загадочным. Пока запись оставалась короче лимита в 10 lookup и все механизмы разрешались нормально, лишний обрывок мог годами лежать незамеченным - например, если строка была написана иначе или добавлена условно. Но воскресное редактирование изменило порядок и длину строки настолько, что при повторном сохранении в панели DNS-провайдера произошла ещё одна правка того же фрагмента - и именно тогда "проснулась" уже готовая опечатка. По спецификации SPF (RFC 7208) если механизм include ссылается на домен, для которого не удаётся получить TXT-запись (в том числе из-за NXDOMAIN), весь результат проверки SPF для этого запроса становится permerror - целиком, а не только для одного механизма. Проверяющий сервер не может сказать "этот include не разрешился, но остальные ок, пропустим" - спецификация требует считать всю запись невалидной. Отсюда и результат: все письма домена, а не только письма нового провайдера, получали permerror, что многие принимающие серверы трактуют строже, чем обычный fail.
Исправление заняло одну строку:
Было:
v=spf1 include:_spf.google.com include:mailgun.org include:sendgrid.con -all
Стало:
v=spf1 include:_spf.google.com include:mailgun.org include:sendgrid.net -all
После правки повторно прогнали ту же проверку по каждому include - все домены резолвились, permerror в свежих DMARC-отчётах исчез в течение суток, доставляемость начала восстанавливаться. Полного возврата к прежним показателям пришлось ждать ещё некоторое время: часть принимающих серверов какое-то время хуже доверяет домену после серии отказов, и репутация отходит постепенно, а не мгновенно по факту починки записи. За общими принципами настройки SPF/DKIM/DMARC с нуля, если делаете это впервые, у нас есть пошаговый разбор - как установить и настроить SPF, DKIM и DMARC на VPS, а если уже настроено и хочется свериться с типичными граблями - частые ошибки SPF, DKIM и DMARC на сервере.
Что изменили после инцидента
Разбор показал не одну проблему, а три системные дыры в процессе, из-за которых опечатка прожила незамеченной и превратилась в недельный инцидент:
- Правки DNS вручную, без ревью. Любое изменение SPF, DKIM или DMARC теперь идёт не через веб-панель регистратора напрямую, а через файл зоны в git-репозитории с обязательным ревью хотя бы одним человеком перед применением. Само по себе ревью не гарантирует, что опечатку заметят глазами, но убирает соблазн "быстро поправить одну строчку в интерфейсе в воскресенье вечером".
- Не было автоматической проверки синтаксиса перед публикацией. Теперь перед применением новой SPF-записи прогоняется тот же скрипт, что и в разборе гипотезы про лимит lookup - только с дополнением: он не просто считает количество запросов, а проверяет, что каждый
include,a,mxиexistsреально резолвится, и падает с ошибкой, если хотя бы один механизм возвращает NXDOMAIN. - Отчёты DMARC никто не мониторил проактивно. До инцидента XML-отчёты складывались в почтовый ящик и открывались, только если кто-то вспоминал о них. Сейчас разбор
rua-отчётов автоматизирован, а результат агрегируется в понятный дашборд с отдельным выделениемpermerror- именно потому что этот статус почти всегда означает "ошибка в самой записи", а не "письмо от плохого IP", и заслуживает более быстрой реакции, чем обычныйfail. Заодно завели общий мониторинг доменных записей и сертификатов на отдельном небольшом сервере, чтобы падение или деградация DNS-зоны попадала в тот же канал уведомлений, что и остальная инфраструктура - подробнее о таком мониторинге у нас есть отдельная статья: мониторинг сертификатов и доменов.
Отдельно обсуждали SPF flattening - замену include-механизмов на статический список IP-адресов, чтобы не зависеть от DNS-разрешения сторонних доменов вообще. У подхода есть обратная сторона: провайдеры периодически меняют свои диапазоны IP без предупреждения, и статический список придётся поддерживать руками или отдельным автоматическим синком - то есть проблема не исчезает, а переезжает в другое место. Команда решила этого не делать и вместо этого сделать упор на проверку перед публикацией и мониторинг после неё - для домена с не самым большим числом провайдеров почты это оказалось проще и надёжнее.
Если для почтовой инфраструктуры вам нужен предсказуемый сервер под скрипты проверки DNS, cron-задачи мониторинга и сам SMTP-релей - лучше держать это на отдельной машине, не привязанной к основному продакшену, чтобы падение одного не тянуло за собой другое.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Может ли одна ошибка в SPF сломать сразу и транзакционные, и маркетинговые письма?
Да, если оба канала отправляют от одного и того же домена - SPF-запись одна на весь домен, а не набор изолированных правил по отправителям. Синтаксическая ошибка в любом её месте портит проверку для всех источников почты сразу.
Чем permerror отличается от fail в DMARC-отчётах?
fail означает, что IP-адрес отправителя проверили и он не входит в разрешённые для домена; permerror означает, что саму SPF-запись не удалось корректно разобрать или один из её механизмов не резолвится через DNS. При permerror разбираться нужно в самой записи, а не в списке отправителей.
Как быстро проверить SPF-запись на синтаксические ошибки?
Раскройте её через dig +short TXT ваш-домен и для каждого include: в отдельности выполните тот же dig по указанному в нём домену - если хотя бы один возвращает пусто или NXDOMAIN, вся запись становится невалидной для проверяющих серверов.
Правда ли, что превышение лимита в 10 DNS-lookup даёт тот же эффект, что и синтаксическая ошибка?
Да, оба случая приводят к permerror по спецификации SPF (RFC 7208), поэтому их легко перепутать на старте расследования. Разница в диагностике: лимит lookup считается по всей цепочке include, а битый include находится точечно через прямой dig по каждому домену.
Нужно ли переходить на статический список IP вместо include, чтобы не зависеть от чужих доменов?
Не обязательно и не всегда оправданно: провайдеры меняют диапазоны IP без предупреждения, и статический список превращается в отдельную задачу поддержки. Для большинства доменов надёжнее автоматическая проверка записи перед публикацией и мониторинг DMARC-отчётов после.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →