Gmail отбивает всю почту: 550-5.7.26 и три сломанные аутентификации разом
Короткий ответ: если 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)
- Решение: чинить SPF, DKIM и FCrDNS одновременно, а проверять — по заголовкам доставленного письма, а не по логам Postfix. Итог:
DKIM-Signatureна месте,spf=pass,dmarc=pass,250 2.0.0 OK. - Альтернатива: отдать транзакционную почту внешнему сервису (Postmark, Unisender, SES) — если некому годами следить за репутацией IP и bounce rate.
- Грабли: OpenDKIM при ненайденном ключе не пишет в лог ничего.
systemctl is-active→activeне доказывает, что подпись ставится. Ручная отправка с самого сервера проходит успешно и создаёт ложное ощущение победы.
Что 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 хостится на Cloudflare | Route 53 (dig NS → ns-*.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, а к тому, что цепочка не замыкается. Проверка занимает две команды:
| |
Это классический 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 не пишет заголовки писем в лог вообще. Я начал именно с этого:
| |
Ноль выглядит убедительно. На самом деле он не значит ничего — этот метод не отличает «не подписывает» от «подписывает». Полчаса я строил гипотезы на показаниях прибора, который ничего не измеряет.
Рабочая проверка ровно одна: отправить письмо на локальный ящик и прочитать заголовки доставленного письма.
| |
Если бы я построил этот способ проверки первым, а чинить начал вторым, каждый следующий шаг сразу показывал бы, помог он или нет. Я сделал наоборот и заплатил за это тремя ложными победами подряд.
Три настоящих дефекта, которые ничего не исправили
Дальше я нашёл три реальные поломки, починил каждую — и подписи не появилось ни разу. Это стоит отдельного раздела, потому что ощущение «я же нашёл проблему» здесь работает против тебя.
| Гипотеза | Как проверял | Вердикт |
|---|---|---|
opendkim.conf обрезан | в файле 4 строки: ни Mode, ни KeyTable, ни SigningTable | Дефект настоящий. Дописал, рестарт — подписи нет |
| Демон не стартует | systemctl is-active opendkim → active, в логе чистый старт | Не дефект |
| Права на приватный ключ | ключ принадлежал _apt:uuidd с 0600, демон работает от opendkim | Дефект настоящий. Исправил — подписи нет |
Про конфиг: каталог /etc/opendkim/ имел mtime Oct 11 2024 при файлах от 2017 года — обновление пакета переписало часть содержимого, а заодно переставило владельца ключа на _apt. Проверять доступ надо от лица сервисного пользователя, а не от root:
| |
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. Общий вывод дороже конкретной опции: тестировать надо тем же путём, которым ходит настоящий трафик. Отправка с самого сервера — не тот путь.
Что чинил по порядку
- SPF. Записи
v=spf1не было вовсе — лежал только SenderID видаspf2.0/mfrom,pra. RFC 7208 требует «discard records that do not begin with a version section of exactlyv=spf1», так что для Gmail этой записи не существовало. Опубликовалv=spf1 ip4:203.0.113.51 include:_spf.google.com include:spf.unisender.com ~all. - DKIM, конфиг. Восстановил
Mode,Canonicalization,KeyTable,SigningTable,InternalHosts,ExternalIgnoreList. - DKIM, владелец ключа.
chown opendkim:opendkimна приватный ключ, проверка черезsu -s /bin/sh opendkim. - DKIM, тип датасета.
refile:→file:дляKeyTableиSigningTable. - DKIM, охват.
TrustedHostsсинхронизирован сmynetworks: 7 → 21. - 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,pra | dig TXT example.org +short | v=spf1 … ~all, spf=pass |
| DKIM | подпись не ставилась, в логе ни строки | заголовки доставленного письма | d=example.org; s=mail, dkim=pass |
| FCrDNS | PTR → чужой хост → чужой IP | dig -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 за семантикой конкретной опции. Молчание — это не «всё хорошо», это отдельный класс поведения, и у него всегда есть описание в документации.
<< Previous Post
|
Next Post >>