Содержание<nav class="toc-nav"> <ul> <li><a href="#главная-идея-мерить-а-не-верить">Главная идея: мерить, а не верить</a></li> <li><a href="#точка-отсчёта-3081-балла">Точка отсчёта: 3.081 балла</a></li> <li><a href="#находка-первая-провайдер-режет-исходящий-25">Находка первая: провайдер режет исходящий 25</a> <ul> <li><a href="#как-доказать-что-режут-на-выходе-а-не-у-вас">Как доказать, что режут на выходе, а не у вас</a></li> </ul> </li> <li><a href="#находка-вторая-ptr-и-почему-его-нельзя-проверить-с-сервера">Находка вторая: PTR, и почему его нельзя проверить с сервера</a></li> <li><a href="#находка-третья-dkim-и-диагноз-в-одной-строке">Находка третья: DKIM и диагноз в одной строке</a> <ul> <li><a href="#как-проверить-dkim-не-отправляя-письмо-наружу">Как проверить DKIM, не отправляя письмо наружу</a></li> </ul> </li> <li><a href="#скрытый-капкан-у-postfix-свой-собственный-etcresolvconf">Скрытый капкан: у Postfix свой собственный /etc/resolv.conf</a></li> <li><a href="#скрытый-капкан-второй-dnsbl-молча-перестаёт-работать">Скрытый капкан второй: DNSBL молча перестаёт работать</a></li> <li><a href="#результат--0201">Результат: -0.201</a></li> <li><a href="#где-я-ошибся">Где я ошибся</a> <ul> <li><a href="#и-отдельно-про-записи-в-зоне">И отдельно про записи в зоне</a></li> </ul> </li> <li><a href="#перед-переключением-mx">Перед переключением MX</a></li> <li><a href="#мина-которую-стоит-разрядить-заранее">Мина, которую стоит разрядить заранее</a></li> <li><a href="#чем-всё-закончилось">Чем всё закончилось</a></li> </ul> </nav>

Поднять почтовый сервер сегодня просто. iRedMail ставится одним скриптом, и через полчаса у вас работающий Postfix, Dovecot, антивирус и веб-панель.

Сложное начинается потом. Сервер работает, письма уходят, в логах 250 Ok - а до адресатов они или доходят в спам, или не доходят вовсе. И самое неприятное: со своей стороны вы этого не увидите никогда. Отправляющий сервер не знает, в какую папку положил ваше письмо Gmail.

Ниже - как я эту слепоту снял, что нашёл, и где ошибся сам.

Главная идея: мерить, а не верить

Весь аудит построен на одной мысли. Нужен принимающий сервер, где у вас есть root.

Онлайн-проверялки SPF и DKIM полезны, но они говорят только «запись синтаксически валидна». Они не скажут, какую оценку поставит вашему письму SpamAssassin на той стороне, а именно это решает судьбу письма.

Если же вы отправите тестовое письмо на ящик, который сами и обслуживаете, то в логах принимающей стороны увидите полный вердикт: разбор по правилам, баллы за каждое, итог и порог. Это объективная оценка со стороны, а не самоуспокоение.

У меня для этого есть второй почтовик. Дальше все цифры - его вердикты.

Точка отсчёта: 3.081 балла

Первое тестовое письмо, DNS ещё не трогал. Вердикт принимающей стороны при пороге 6.2:

score 3.081

SPF_SOFTFAIL=0.972
SPF_HELO_SOFTFAIL=0.896
DMARC_NONE=0.898
DKIM_INVALID=0.1
DKIM_SIGNED=0.1
CTE_8BIT_MISMATCH=0.113

Письмо прошло - у дружественного сервера порог высокий. Но смотрите на состав: около 2.87 балла из 3.081 это чистая плата за ненастроенный DNS. С таким набором у Gmail или Outlook дорога одна, и она не в инбокс.

Вот ради этой разбивки всё и затевалось. Теперь понятно не «что-то не так», а что именно и сколько это стоит.

Находка первая: провайдер режет исходящий 25

Самое обидное обнаружилось раньше всего. Письма наружу не уходили вообще, а telnet на чужой почтовик давал ENETUNREACH при полностью живой сети.

Многие хостеры закрывают исходящий TCP/25 на виртуальных машинах по умолчанию, чтобы с них не спамили. Входящий 25 при этом работает - и это сбивает с толку: приём почты есть, отправки нет. Открывается тикетом в поддержку.

Проверяйте это первым делом на любом новом почтовике у облачного провайдера. Пока 25 закрыт, вся остальная диагностика бессмысленна: вы будете чинить DKIM у сервера, который физически не может никуда дотянуться.

Как доказать, что режут на выходе, а не у вас

Тут легко утонуть в догадках: может, фаервол, может, маршрут, может, провайдер. Методика простая и однозначная.

Возьмите хост, который слушает и 25, и 443 (или 22), и постучите с проверяемой машины на оба порта. У меня получилось так:

443 - 4 мс
22  - 21 мс
25  - таймаут 10 с

Один адресат, один момент времени, одна маршрутизация. Разница только в номере порта. Это не оставляет места для трактовок.

Для полноты картины стоит показать, что дело не в локальных правилах:

nft list chain inet filter output   # пусто, policy accept
ip route show                        # нет blackhole/prohibit

Негодный тест, на который я потратил целый прогон: стучать по 25, 443, 587, 465 на MX какого-нибудь крупного почтовика. Его MX слушает только 25, поэтому таймауты на остальных портах появятся всегда, независимо от вашей проблемы. Доказывает ровно ничего.

Находка вторая: PTR, и почему его нельзя проверить с сервера

Обратная запись - обязательное условие. Без неё приличный принимающий сервер встретит вас прохладно.

Две неочевидные вещи.

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

И главное: проверять rDNS с самого сервера бесполезно. iRedMail при установке прописывает имя хоста в /etc/hosts. Поэтому:

getent hosts 192.0.2.10
# 192.0.2.10  mail.example.com

выглядит правильным ответом при полностью отсутствующем PTR. Я на это чуть не купился. Проверять только внешним резолвером:

dig +short -x 192.0.2.10 @8.8.8.8

Нужен полный FCrDNS: PTR указывает на имя, а прямая запись этого имени возвращается на тот же адрес. Когда сошлось, принимающая сторона начинает писать в логе reverse_client_name=mail.example.com вместо unknown.

Находка третья: DKIM и диагноз в одной строке

Вот этот признак стоит запомнить и выписать на бумажку:

Authentication-Results: dkim=fail (1024-bit key)
  reason="fail (OpenSSL error: data too large for key size)"

Формулировка data too large for key size означает ровно одно: сервер подписывает письмо 2048-битным ключом, а в DNS опубликован 1024-битный. Подпись длиннее, чем может проверить опубликованный ключ.

Это не абстрактное «DKIM почему-то не работает». Это прямое указание на рассинхрон ключа - скорее всего, в зоне лежит старая запись от прошлой инсталляции или от предыдущего провайдера почты.

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

Как проверить DKIM, не отправляя письмо наружу

Приём, который сильно экономит нервы, особенно когда 25 ещё закрыт:

sendmail -f postmaster@example.com admin@example.net <<'EOF'
Subject: dkim check
From: postmaster@example.com
To: admin@example.net

test
EOF

sleep 15
postqueue -p                    # взять QID
postcat -q <QID> | grep -i dkim # смотреть d= и s=
postsuper -d <QID>              # убрать из очереди

При закрытом 25 письмо гарантированно залипнет в очереди, и наружу ничего не уйдёт. Вы при этом увидите реальную подпись, которую сервер поставил: селектор и домен. В логах антивирусного фильтра тоже полезно поискать строку вида dkim_new=<селектор>:<домен>.

Скрытый капкан: у Postfix свой собственный /etc/resolv.conf

Это, пожалуй, самое ценное из всей статьи, потому что ловит людей надолго.

Postfix работает в chroot. И у него в /var/spool/postfix/etc/ лежат свои копии системных файлов:

resolv.conf
hosts
services
nsswitch.conf

Вы правите /etc/resolv.conf, перечитываете конфигурацию, и ничего не меняется. Потому что postscreen и smtpd смотрят в копию внутри chroot, а она осталась прежней.

Расхождение показывает штатная проверка:

postfix check

И ключевой момент:

systemctl restart postfix   # пересобирает chroot
systemctl reload postfix    # НЕ пересобирает

То есть reload, которым мы по привычке применяем правки, в этом случае бесполезен. Я потерял на этом время, пока не сопоставил.

Скрытый капкан второй: DNSBL молча перестаёт работать

Ещё одна тихая поломка, которая не оставляет следов в логах.

postscreen умеет сверять подключающихся с чёрными списками. Конфигурация обычно выглядит так:

postscreen_dnsbl_sites = zen.spamhaus.org=127.0.0.[2..11]*3
                         b.barracudacentral.org=127.0.0.2*2
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_action = drop

Проблема в том, что крупные чёрные списки отказывают запросам, идущим через публичные резолверы. Если на сервере в resolv.conf прописан 8.8.8.8 или 1.1.1.1, Spamhaus и Barracuda вернут вам 127.255.255.254 - технический код «запрос через публичный резолвер, обслуживать не будем».

А дальше арифметика: 127.255.255.254 не попадает в диапазон 127.0.0.[2..11], указанный в конфиге. Значит, ни одно совпадение не засчитается. Фильтрация отваливается полностью и совершенно молча, ошибок в логе нет, в интерфейсе всё зелёное.

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

# /etc/unbound/unbound.conf.d/10-mail.conf
server:
    interface: 127.0.0.1
    # никаких forward-zone: нужна рекурсия от корневых серверов

Проверить резолвер можно тестовыми векторами - у Spamhaus для этого есть специальные адреса:

dig +short 2.0.0.127.zen.spamhaus.org   # должно быть 127.0.0.2 / 4 / 10
dig +short 1.0.0.127.zen.spamhaus.org   # должно быть NXDOMAIN

Если оба запроса отвечают одинаково - резолвер негодный, фильтр не работает. Это проверка на тридцать секунд, а находит поломку, которую иначе не заметишь годами.

Результат: -0.201

После настройки SPF, DKIM, DMARC и PTR повторил ровно тот же сквозной тест. Вердикт той же принимающей стороны:

score -0.201

SPF_PASS=-0.001
SPF_HELO_PASS=-0.001
DMARC_PASS=-0.001
DKIM_VALID=-0.1
DKIM_VALID_AU=-0.1
DKIM_VALID_EF=-0.1
DKIM_SIGNED=0.1

Authentication-Results: dkim=pass (2048-bit key) header.d=example.com

С 3.081 до -0.201, улучшение на 3.28 балла, итог ушёл в отрицательную зону. Подпись проверяется, оба варианта SPF проходят, DMARC сходится.

Записи, которые это дали:

example.com          TXT  "v=spf1 ip4:192.0.2.10 ip4:192.0.2.20 ~all"
mail.example.com     TXT  "v=spf1 ip4:192.0.2.10 -all"
dkim._domainkey      TXT  "v=DKIM1; k=rsa; p=<2048-битный ключ сервера>"
_dmarc               TXT  "v=DMARC1; p=none; rua=mailto:postmaster@example.com; fo=1; adkim=r; aspf=r"

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

Где я ошибся

Три честных промаха, каждый чему-то научил.

Я занизил прогноз почти втрое. Ожидал около 1.0 балла, рассчитывая, что правило DMARC_NONE останется на месте: политика-то у меня p=none, то есть «ничего не делать с нарушителями». Логично же?

Нет. Сработал DMARC_PASS. DMARC проходит, когда SPF или DKIM выровнены с доменом в заголовке From, и строгость политики тут не при чём. p=none говорит принимающей стороне, что делать с провалом проверки, а не отменяет саму проверку. Это стоит усвоить сразу, я усваивал через неверный прогноз.

Я принял свой же косяк за проблему сервера. В первом тесте светилось CTE_8BIT_MISMATCH=0.113. Я записал это в список дефектов конфигурации. На самом деле это артефакт тестового письма, которое я собрал руками: объявил utf-8, а отдал восьмибитный текст без корректного Content-Transfer-Encoding. К настройке сервера отношения не имеет никакого.

Мораль: когда генерируете тестовые письма сами, вы вносите в результат собственные ошибки. Сомнительные правила стоит проверять на письме, отправленном нормальным клиентом.

Я поторопил грейлистинг и сделал хуже. Первое письмо получило от принимающей стороны 451 4.7.1 Intentional policy rejection - штатный грейлистинг, который просит прийти позже. Я, вместо того чтобы подождать, дёрнул принудительную попытку отправки.

Результат: в логах принимающей стороны «Client retries too soon, greylisted again» и инкремент счётчика блокировок. На время разблокировки это не повлияло - оно считается от момента первой попытки плюс фиксированный интервал, - но пользы не принесло ни на грамм. Грейлистинг нужно просто переждать, доставка случилась через пятнадцать минут сама.

И отдельно про записи в зоне

Запись DMARC я сначала создал не там. Вместо _dmarc она оказалась на хосте mail, а на _dmarc остался голый v=DMARC1; p=none без адреса для отчётов.

На результат теста это не повлияло, DMARC_PASS сработал и так. Но отчётов бы я не получил никогда: DMARC читается строго с _dmarc.<домен>, и запись в любом другом месте просто никем не запрашивается.

Проверяется элементарно:

dig +short TXT _dmarc.example.com

Если пусто или там нет rua= - отчётов не будет, сколько бы похожих записей ни лежало в зоне рядом.

Перед переключением MX

Когда всё сошлось, остаётся сменить MX. Два правила, которые я вывел на практике.

Снизьте TTL заранее. Я поставил 300 секунд на всех задействованных записях. Если что-то пойдёт не так, откат займёт пять минут, а не сутки.

Сверяйте все авторитативные серверы по отдельности, а не через кеш. У моего регистратора четыре NS в двух разных доменах, и репликация между ними не мгновенная. DKIM догнал на последнем из них примерно за семь минут, SPF - ещё минут за шесть после этого. Причём «отстающий» сервер оказался стабильно одним и тем же.

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

for ns in ns1.registrar.tld ns2.registrar.tld ns3.registrar.tld ns4.registrar.tld; do
  echo -n "$ns: "
  dig +short +norecurse TXT dkim._domainkey.example.com @"$ns"
done

Все четыре должны отдать одно и то же. Только после этого MX.

Мина, которую стоит разрядить заранее

В конфигурации подписи DKIM бывает catch-all правило - сопоставление домена отправителя с ключом по шаблону '.', то есть «любой домен».

Выглядит удобно, работает плохо. Любой новый домен, который вы потом добавите на этот сервер, автоматически подпишется ключом чужого домена. Получатель увидит d= одного домена, адрес отправителя другого, и честно поставит dkim=fail.

Лучше перечислить домены явно. Отсутствие подписи безопаснее неверной: в первом случае вы теряете бонус, во втором получаете штраф и выглядите как подделка.

Чем всё закончилось

Через сутки после переключения MX в ящике postmaster лежал первый агрегированный DMARC-отчёт от крупного почтового сервиса, с dkim=pass по моему домену. То есть rua= работает, отчёты собираются, и теперь я вижу не только свои тесты, но и статистику по всей реальной почте домена.

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

Заведите себе второй почтовик, хоть на самом дешёвом тарифе. Он окупится первым же разбором.