BitPage

Gmail отбивает всю почту: 550-5.7.26 и три сломанные аутентификации разом

 · 9 мин чтения

Короткий ответ: если Gmail отбивает письма с кодом 550-5.7.26, причина почти никогда не одна. У меня на легаси-релее одновременно не работали три механизма: SPF-записи не было вовсе (вместо неё лежал мёртвый SenderID), DKIM не подписывал письма из-за типа датасета в конфиге OpenDKIM (refile: там, где нужен file:), а PTR указывал на хост, чья A-запись вела на другой сервер. Чинить пришлось все три — пока сломан хотя бы один, счётчик остаётся bounced=1429 против sent=5 в сутки.

Сервер — исходящий релей для транзакционной почты: восстановление пароля, уведомления, письма от приложений примерно с 20 хостов. Postfix плюс OpenDKIM 2.10.3 в легаси-контейнере OpenVZ, DNS в Route 53, входящая почта домена идёт мимо — в Google Workspace. Аптайм 753 дня, конфиги правились последний раз в 2017 году, контейнер вне Ansible. Никто ничего не менял; поменялся Gmail.

Коротко (TL;DR)

Что Gmail требует с 2024 года и почему сервер сломался «сам»

Требования ужесточились в феврале 2024, и легаси-конфиг, который до этого годами работал, перестал им соответствовать без единой правки на сервере. Руководство для отправителей Google требует от всех отправителей аутентификации хотя бы одним механизмом, а от массовых (от 5000 писем в сутки на адреса Gmail) — SPF и DKIM, плюс DMARC, плюс валидные прямая и обратная DNS-записи, плюс долю жалоб на спам ниже 0,3 %.

Отсюда главная ловушка этого инцидента: нет события «изменили X — упало Y». Искать «что я вчера сломал» бесполезно, потому что вчера ничего не ломали. Сломалось соответствие требованиям, которые переехали.

Дословно Gmail говорил вот что:

550-5.7.26 Your email has been blocked because the sender is unauthenticated.
550-5.7.26 Gmail requires all senders to authenticate with either SPF or DKIM.
550-5.7.26  Authentication results:
550-5.7.26  DKIM = did not pass
550-5.7.26  SPF [example.org] with ip: [203.0.113.51] = did not pass

Gmail честно перечислил оба провалившихся механизма — и SPF, и DKIM. Я это прочитал, но всё равно полез чинить их по очереди, ожидая после каждого шага, что письма пойдут. Не шли.

Почему я начал не с того: собственная заметка врала

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

Что было записаноЧто оказалось на самом деле
PTR в порядке, прямая и обратная сходятсяPTR вёл на legacy.example.org, чья A-запись — 198.51.100.216, другой сервер
DNS хостится на CloudflareRoute 53 (dig NSns-*.awsdns-*)
DMARC отсутствуетDMARC на месте: v=DMARC1; p=none
Почта отбивается GmailВерно

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

Что на самом деле значит 550-5.7.25 про PTR

Формулировка Gmail про PTR состоит из двух половин, и вторая важнее первой. Полностью она такая:

550-5.7.25 [203.0.113.51] The IP address sending this message does not have a PTR
550-5.7.25 record setup, or the corresponding forward DNS entry does not match the sending IP

Я, как и все, прочитал до слов «does not have a PTR record», проверил dig -x, увидел ответ и поставил галочку. А сообщение говорит «or the corresponding forward DNS entry does not match» — то есть претензия может быть не к отсутствию PTR, а к тому, что цепочка не замыкается. Проверка занимает две команды:

1
2
dig -x 203.0.113.51 +short        # -> legacy.example.org.
dig A legacy.example.org +short   # -> 198.51.100.216   (не тот IP)

Это классический FCrDNS: IP → имя → обратно должен получиться тот же IP. Здесь обратно получался чужой сервер. Чинить через A-запись legacy.example.org было нельзя — хост живой и обслуживает посторонний сервис. Поэтому переклеил PTR в Hetzner Robot на mail.example.org, чья A-запись уже вела куда надо, и заодно выставил myhostname = mail.example.org, чтобы HELO совпадал с PTR. Побочных эффектов — ноль.

Почему grep по mail.log ничего не доказывает про DKIM

Проверять наличие DKIM-подписи через лог Postfix нельзя: Postfix не пишет заголовки писем в лог вообще. Я начал именно с этого:

1
grep -c "DKIM-Signature" /var/log/mail.log   # -> 0

Ноль выглядит убедительно. На самом деле он не значит ничего — этот метод не отличает «не подписывает» от «подписывает». Полчаса я строил гипотезы на показаниях прибора, который ничего не измеряет.

Рабочая проверка ровно одна: отправить письмо на локальный ящик и прочитать заголовки доставленного письма.

1
2
3
4
5
6
sendmail -f noreply@example.org nobody@localhost <<'EOF'
Subject: dkim test

test
EOF
grep -A3 "DKIM-Signature" /var/mail/nobody

Если бы я построил этот способ проверки первым, а чинить начал вторым, каждый следующий шаг сразу показывал бы, помог он или нет. Я сделал наоборот и заплатил за это тремя ложными победами подряд.

Три настоящих дефекта, которые ничего не исправили

Дальше я нашёл три реальные поломки, починил каждую — и подписи не появилось ни разу. Это стоит отдельного раздела, потому что ощущение «я же нашёл проблему» здесь работает против тебя.

ГипотезаКак проверялВердикт
opendkim.conf обрезанв файле 4 строки: ни Mode, ни KeyTable, ни SigningTableДефект настоящий. Дописал, рестарт — подписи нет
Демон не стартуетsystemctl is-active opendkimactive, в логе чистый стартНе дефект
Права на приватный ключключ принадлежал _apt:uuidd с 0600, демон работает от opendkimДефект настоящий. Исправил — подписи нет

Про конфиг: каталог /etc/opendkim/ имел mtime Oct 11 2024 при файлах от 2017 года — обновление пакета переписало часть содержимого, а заодно переставило владельца ключа на _apt. Проверять доступ надо от лица сервисного пользователя, а не от root:

1
2
su -s /bin/sh opendkim -c "head -c 20 /etc/opendkim/default/mail.private"
# head: невозможно открыть ... Отказано в доступе

chown opendkim:opendkim — обязателен. Но подписи после него так и не было. В этот момент гипотезы у меня кончились, и это оказалось лучшим, что случилось за весь инцидент.

Почему OpenDKIM молчал: file: против refile:

Причина нашлась в man-странице на самом сервере — в одном абзаце про тип датасета, от которого зависит, найдётся ключ или нет. Читал я именно локальный man opendkim.conf от версии 2.10.3: он авторитетнее любой веб-копии, потому что описывает ровно тот бинарник, который у тебя запущен. Та же страница онлайн описывает поиск в SigningTable так:

If this table specifies a regular expression file (“refile”), then the keys are wildcard patterns that are matched against the address found in the From: header field. […] For all other database types, the full user@host is checked first, then simply host, then user@.domain

То есть при refile: шаблон сопоставляется с адресом целиком. В таблицах у меня лежали записи простого формата:

example.org  mail._domainkey.example.org

А подключены они были как refile:. Шаблон example.org сопоставляется с адресом noreply@example.org — не совпадает. Ключ не найден, письмо уходит неподписанным, и в логе не появляется ни одной строки. При file: вторым шагом ищется «simply host» — совпадает, ключ находится.

Фикс — одно слово в двух строках:

KeyTable                file:/etc/opendkim/KeyTable
SigningTable            file:/etc/opendkim/SigningTable

Альтернатива — оставить refile: и переписать записи в вид *@example.org. Я её отверг: таблицы описывали 5 доменов, и править пять строк ради сохранения неподходящего типа датасета рискованнее, чем поменять тип под уже существующий формат.

Почему ручной тест проходил, а почта приложений уходила голой

Вторая половина той же поломки — слишком узкий InternalHosts, и она не видна ни одним тестом с самого сервера. Man описывает опцию так: «Identifies a set internal hosts whose mail should be signed rather than verified» — то есть хосты вне списка не подписываются, а проверяются.

Список указывал на TrustedHosts с 7 адресами. В mynetworks у Postfix их было 23. Хосты приложений в пересечение не попадали — их почта верифицировалась вместо подписи и уходила голой. А мой ручной тест шёл с 127.0.0.1, который в списке был, и проходил успешно.

InternalHosts           refile:/etc/opendkim/TrustedHosts

Здесь refile: уместен: TrustedHosts действительно содержит шаблоны и подсети. Синхронизировал список с mynetworks: 7 адресов → 21. Общий вывод дороже конкретной опции: тестировать надо тем же путём, которым ходит настоящий трафик. Отправка с самого сервера — не тот путь.

Что чинил по порядку

  1. SPF. Записи v=spf1 не было вовсе — лежал только SenderID вида spf2.0/mfrom,pra. RFC 7208 требует «discard records that do not begin with a version section of exactly v=spf1», так что для Gmail этой записи не существовало. Опубликовал v=spf1 ip4:203.0.113.51 include:_spf.google.com include:spf.unisender.com ~all.
  2. DKIM, конфиг. Восстановил Mode, Canonicalization, KeyTable, SigningTable, InternalHosts, ExternalIgnoreList.
  3. DKIM, владелец ключа. chown opendkim:opendkim на приватный ключ, проверка через su -s /bin/sh opendkim.
  4. DKIM, тип датасета. refile:file: для KeyTable и SigningTable.
  5. DKIM, охват. TrustedHosts синхронизирован с mynetworks: 7 → 21.
  6. FCrDNS. PTR переклеен на mail.example.org, myhostname приведён к тому же имени.

Итоговый рабочий фрагмент /etc/opendkim.conf:

Mode                    sv
Canonicalization        relaxed/simple
KeyTable                file:/etc/opendkim/KeyTable
SigningTable            file:/etc/opendkim/SigningTable
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts

Сводка по трём механизмам:

МеханизмЧто былоЧем проверятьЧто стало
SPFтолько мёртвый SenderID spf2.0/mfrom,pradig TXT example.org +shortv=spf1 … ~all, spf=pass
DKIMподпись не ставилась, в логе ни строкизаголовки доставленного письмаd=example.org; s=mail, dkim=pass
FCrDNSPTR → чужой хост → чужой IPdig -x IP + dig A <ответ>PTR → mail.example.org → тот же IP

После шестого шага письмо на Gmail получило 250 2.0.0 OK.

Что осталось сломанным: bounce rate 61 %

Аутентификация починена, но репутация — нет, и это отдельная задача не для админа, а для разработчиков приложения. Из логов: 687 отказов против 436 доставленных, то есть 61 % писем уходит в никуда. Приложение годами шлёт на несуществующие адреса, включая опечатки в доменах — на @iclaud.com за сутки ушло 104 письма.

При требуемых Google 0,3 % жалоб такой bounce rate рано или поздно уронит репутацию IP, и письма поедут в «Спам» уже при идеальных SPF и DKIM.

Отдельно красивый механизм самосокрытия проблемы: сервер слал отчёты о недоставке на адрес отправителя, домен которого сам же считал локальным через mydestination. Локального пользователя с таким именем не было, отчёт уничтожался. Круг замкнут: приложение шлёт на мёртвый адрес → отчёт об этом уничтожается → никто не узнаёт → приложение продолжает слать. 348 уничтоженных отчётов в сутки.

Ещё не решено: envelope sender совпадает с From (правильно — отдельный технический адрес возврата с автоматической обработкой), certbot 0.12.0 стучится в отключённый acme-v01, сертификат просрочен с сентября 2021, и все правки сделаны руками на сервере вне Ansible. Последнее — честный техдолг: в репозитории этого контейнера не существует, и следующий человек ничего не найдёт.

Итог

Если Gmail отбивает почту с легаси-релея, не чините первую найденную причину — сначала постройте способ проверки, который не врёт. Для DKIM это заголовки доставленного письма, для FCrDNS — dig -x с обратной проверкой A-записи, для SPF — dig TXT с проверкой, что запись начинается ровно с v=spf1. И держите в голове главное свойство OpenDKIM: при ненайденном ключе он не пишет в лог ничего, поэтому «демон активен, конфиг валиден, ключ читается» — это ноль доказательств того, что подпись ставится.

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

#Postfix #OpenDKIM #SPF #DKIM #почта

<< Previous Post

|

Next Post >>