Sender-dependent relay в Postfix: разные релеи под каждый тип отправителя
Глобальный relayhost гонит транзакционку, рассылки и корпоративную почту через один IP и одни креды — и топит репутацию. Разбираем, как одним инстансом Postfix развести отправку по SendGrid, Amazon SES и Postmark в зависимости от envelope-отправителя, с индивидуальным SASL на каждого. И почему выбор идёт по MAIL FROM, а не по заголовку From:.
EvilMail Team11 июля 2026 г.11 мин чтения
Один relayhost на всех — почему это стоит вам репутации
relayhost = [smtp.provider.net]:587 — глобальный параметр. Значение ровно одно на весь Postfix. И этого достаточно, пока с сервера уходит один тип почты.
Реальность обычно другая. С одного домена летят три несовместимых потока: транзакционные письма (сброс пароля, чеки, коды подтверждения — доставка критична, задержка недопустима), маркетинговые рассылки (высокий bounce и complaint rate по определению) и живая корпоративная переписка. Когда всё это уходит через один smarthost, один IP-пул и одни SASL-креды, происходит ровно то, чего боишься: пара сотен жалоб «спам» на рассылку роняет репутацию отправляющего IP, и следом в спам начинают падать письма со сбросом пароля. Клиент не получает код, пишет в поддержку, и уже вы объясняете, почему «письмо не пришло».
Лечится это изоляцией потоков: разные релеи, разные IP-пулы провайдеров, разные креды. Транзакционку — на Postmark (он заточен именно под неё и держит отдельные пулы), рассылки — на Amazon SES с выделенным IP, всё остальное — на SendGrid. Но поднимать три инстанса Postfix ради этого не нужно: получите тройной комплект конфигов, которые разъедутся при первой же правке. И чистый transport_maps тут тоже не решение — он маршрутизирует, но не даёт per-sender SASL, а именно разные креды нам и нужны.
Побеждает первое совпадение. Ключевой момент: sender_dependent_relayhost_maps переопределяет глобальный relayhost для конкретного отправителя, но проигрывает transport_maps. Если вы уже гоняете что-то через transport, sender-dependent карта до этого письма не доберётся.
Теперь два факта, на которых ломается большинство внедрений.
Ключ поиска — это envelope sender (MAIL FROM), а не заголовок `From:`. Это разные вещи. From:, который видит получатель в почтовом клиенте, и MAIL FROM конверта, который участвует в SMTP-транзакции, могут отличаться. Postfix смотрит на конверт — причём уже после применения sender_canonical_maps. Скрипт, который шлёт от [email protected] в заголовке, но с пустым или системным конвертом, поедет мимо вашей карты.
Порядок перебора ключей отправителя фиксирован:user+extension@domain → user@domain → @domain. Голый localpart без домена Postfix НЕ пробует. Значит @example.com в таблице ловит весь домен как fallback, а конкретный [email protected] переопределяет его по специфичности.
И отдельно: пустой MAIL FROM (а это все bounce-сообщения и DSN — они по RFC отправляются от <>) не матчится ни на одну строку карты и уходит через relayhost или напрямую по MX.
Базовая конфигурация: развести три релея
Начнём с main.cf. Директивы маршрутизации:
ini
# Выбор релея по envelope-отправителю
sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
# Глобальный fallback для всего, что не попало в карту
# (в т.ч. bounce с пустым MAIL FROM)
relayhost =
Теперь сама карта /etc/postfix/sender_relay. Слева — envelope-отправитель, справа — next-hop в bracket-синтаксисе. Квадратные скобки [host] означают «это конечный хост, не делай MX-lookup» — обязательно для релеев, иначе Postfix полезет искать MX у smtp.sendgrid.net и в лучшем случае попадёт не туда:
Логика специфичности работает так: письмо от [email protected] не находит точного совпадения, падает до @example.com и уходит в SendGrid. Письмо от [email protected] находит точную строку и едет в Postmark. А до @example.com оно бы и не долетело: txn.example.com — самостоятельный домен, и для этих карт Postfix не считает @example.com совпадением для поддомена. Поэтому держите поддомены под потоки — карта читается однозначно и без сюрпризов.
Обратите внимание на регион в SES-эндпоинте: email-smtp.eu-west-1.amazonaws.com — это Ирландия. Эндпоинт региональный, возьмите свой из консоли SES под тот регион, где верифицирован домен, иначе получите таймаут на коннекте к чужому региону.
Индивидуальный SASL на каждого отправителя
Вот та часть, где transport_maps бессилен, а sender-dependent карта — нет. По умолчанию Postfix логинится на релее кредами, привязанными к хосту (next-hop). Три релея — три записи по хостам, и внутри одного релея все отправители используют один логин. Часто это не то, что нужно: у SES-аккаунта могут быть разные SMTP-креды под разные проекты, а у Postmark токен привязан к конкретному message stream.
С smtp_sender_dependent_authentication = yes ключом в smtp_sasl_password_maps становится отправитель, а не хост. Теперь /etc/postfix/sasl_passwd выглядит так — и структурно повторяет карту релеев, что удобно:
SendGrid: username буквально строка apikey, а пароль — сам ключ SG.xxx. Не имя пользователя аккаунта.
Amazon SES: это НЕ ваши IAM Access Key / Secret. SES выдаёт отдельные SMTP-креды (кнопка «Create SMTP credentials»), пароль вычисляется из secret по региону.
Postmark: username и password — один и тот же Server API Token.
smtp_tls_security_level = encrypt заставляет Postfix требовать STARTTLS и рвать соединение, если TLS не поднялся, — на submission-порт 587 это правильное поведение, креды в открытом виде уходить не должны.
Собираем и жёстко ограничиваем права — в файле лежат боевые токены:
Envelope против заголовка From. Cron-задачи, logwatch, скрипты бэкапа шлют письма от root@hostname — с системным MAIL FROM, которого нет в карте. Они проваливаются в relayhost (а если он пустой — пытаются слать напрямую и упираются в блокировку 25-го порта). Лечится переписыванием конверта до lookup:
Теперь системная почта приходит от [email protected], матчится на @example.com и уезжает через SendGrid с нормальной аутентификацией.
Bounce с пустым MAIL FROM. Возвраты и DSN отправляются от <> и в принципе не могут попасть в карту. Это by design — держите рабочий relayhost как fallback либо смиритесь, что они пойдут по MX.
Порт 465 нельзя задать per-destination в main.cf. Implicit TLS (wrappermode) — это свойство транспорта, а не карты. Если конкретный релей слушает только 465, заводите отдельный транспорт в master.cf:
DKIM подписывается milter-ом (OpenDKIM или rspamd) на этом же Postfix — до отдачи письма на релей. Порядок важен: сначала ваша подпись d=example.com, потом уже релей. Некоторые провайдеры добавляют собственную подпись своим d=, это нормально — ваша всё равно валидна, если релей не переписывает тело.
postmap после каждой правки. Любое изменение hash:-таблицы требует пересборки .db — иначе Postfix читает старую скомпилированную версию и вы отлаживаете призрак. С lmdb: то же самое. Держите это на уровне мышечной памяти.
Проверка и отладка
Первое — убедиться, что применилось именно то, что вы написали:
bash
postconf -n | grep -E 'sender_dependent|sasl'
Проверяем карту ДО отправки, не гадая — прогоняем ключ через тот же lookup, что делает Postfix:
Если тут пусто — карта не собралась правильно или ключ не тот. Дальше — реальная тестовая отправка с явно заданным конвертом (флаг -f ставит MAIL FROM):
Строка relay= показывает, куда реально ушло письмо. Если там ваш relayhost вместо ожидаемого провайдера — значит MAIL FROM не совпал с ключом (частая причина — заголовочный From: правильный, а конверт нет). Если видите SASL authentication failed — ключ в sasl_passwd не совпадает с ключом в sender_relay либо забыли smtp_sender_dependent_authentication = yes, и Postfix ищет пароль по хосту. При TLS-проблемах включайте smtp_tls_loglevel = 1, при глухом молчании — -v на транспорте в master.cf для полного трейса SMTP-диалога.
Чек-лист внедрения
Карта sender_relay собрана через postmap, значения в bracket-синтаксисе [host]:port.
smtp_sender_dependent_authentication = yes — иначе SASL идёт по хосту, а не по отправителю.
sasl_passwd keyed by sender, права 600 на файл и .db, боевые токены не в git.
SPF-запись содержит include всех релеев (sendgrid.net, amazonses.com, spf.mtasv.net).
DKIM-milter подписывает на этом Postfix — до отдачи на релей.
sender_canonical_maps переписывает root@/системную почту в реальный адрес из карты.
Для порта 465 — отдельный транспорт relay465 в master.cf с smtp_tls_wrappermode=yes.
Рабочий relayhost как fallback для bounce с пустым MAIL FROM.
Проверка: postmap -q "..." для каждого ключа, тест sendmail -f, relay= в логе показывает верный next-hop.
Один инстанс, одна карта, полная изоляция потоков. Транзакционка больше не зависит от того, как сегодня себя ведёт маркетинговая рассылка, — а именно ради этого всё и затевалось.