Два Postfix на одном сервере: разделяем приём и отправку через postmulti
Один Postfix на 25-м порту — это одна очередь, одна репутация IP и один HELO на весь сервер. Разносим приём и отправку на два независимых инстанса штатным postmulti: раздельные очереди, egress-IP, TLS-политика и DKIM — без второго сервера и без контейнеров.
EvilMail Team10 июля 2026 г.14 мин чтения
03:00. Прод падает не потому, что что-то сломалось, а потому, что всё работает как задумано. Ночная рассылка выплюнула 40 000 писем, половина ушла в deferred из-за грейлистинга на принимающей стороне, active-очередь встала под нагрузкой, а qmgr честно перебирает эту гору. И ровно в этот момент письма ваших клиентов, которые прилетают к вам по MX на 25-й порт, начинают тормозить и копиться. Почему? Потому что это один Postfix с одной/var/spool/postfix, и приём с отправкой дерутся за одну и ту же очередь, один qmgr и один пул процессов.
Это не баг конфигурации. Это архитектура по умолчанию. Монолитный Postfix связывает приём и отправку в одно процессное дерево, хотя у этих двух задач разные требования к репутации, TLS, rate-limit и SLA. Хорошая новость: чинить это не нужно вторым сервером или Docker. В ядре Postfix с версии 2.6 живёт postmulti — механизм multi-instance, который разносит приём и отправку на два независимых инстанса с общими бинарниками. Ниже — как это разворачивается в проде.
Почему один инстанс — это узел связанности
Разберём, что физически общее у монолитного Postfix и почему каждый пункт превращается в проблему.
Единая `/var/spool/postfix`. Очереди incoming, active, deferred, bounce — общие для входящих и исходящих. Забитый deferred от рассылки тормозит планировщик для входящих. Один qmgr на всё.
Единый `inet_interfaces` и `smtp_bind_address`. Нельзя развести приёмный и отправляющий IP. Письмо, принятое на ваш MX-адрес, уходит наружу с того же IP — и репутация приёма страдает от исходящих спам-жалоб.
Единый `myhostname`. Один HELO на весь сервер. А исходящему нужен HELO, совпадающий с PTR его egress-IP, иначе Gmail и Outlook снижают доверие. У приёма и отправки PTR разные — совместить в одном myhostname невозможно.
Единая TLS-политика. Входящему нужен smtpd_tls_security_level = may (принимаем от кого угодно, шифруем оппортунистически). Исходящему в 2026 хочется smtp_tls_security_level = dane с DNSSEC. В монолите это компромисс.
Общий anvil rate-limiter и пул процессов. Прогрев исходящего IP с искусственными лимитами задевает приём.
Приём и отправка — две разные системы с противоположными приоритетами. Держать их в одном процессном дереве — значит соглашаться, что инцидент в одной половине бьёт по другой.
Модель postmulti: что делится, а что общее
Multi-instance устроен просто: бинарники общие, состояние раздельное.
Общими остаются daemon_directory (/usr/libexec/postfix) и command_directory (/usr/sbin) — то есть один apt-пакет обслуживает все инстансы, и апгрейд обновляет их разом. Раздельными у каждого инстанса становятся:
config_directory — /etc/postfix-in, /etc/postfix-out (свои main.cf и master.cf);
Управляет всем default-инстанс (/etc/postfix) — он держит multi_instance_directories со списком дочерних конфигов и работает дирижёром. Инстансы объединяются в группы (multi_instance_group), у каждого свои multi_instance_name и флаг multi_instance_enable. Команда postfix start на default-инстансе поднимает всю группу разом.
Создаём инстансы через postmulti
Сначала включаем multi-instance на default-инстансе. Это добавляет multi_instance_wrapper и переводит postfix в режим дирижёра:
bash
# включить multi-instance на default-инстансе
postmulti -e init
# входящий инстанс в группе postin
postmulti -I postfix-in -G postin -e create
# исходящий инстанс в группе postout
postmulti -I postfix-out -G postout -e create
# пометить оба к автозапуску
postmulti -i postfix-in -e enable
postmulti -i postfix-out -e enable
# проверить раскладку
postmulti -l
После create появляются каталоги /etc/postfix-in и /var/spool/postfix-in с базовым main.cf, где уже прописаны multi_instance_name, multi_instance_group и правильные пути. postmulti -l покажет таблицу: имя, группа, флаг enable и config_directory. Default-инстанс осознанно оставляем пустым и выключенным — он только оркестрирует группу, сам почту не обрабатывает.
Входящий инстанс: принять на 25 и отдать дальше
/etc/postfix-in/main.cf — минимальный приёмник. Слушает свой приёмный IP, TLS оппортунистический, никаких клиентских исходящих настроек, свой syslog_name для разделения логов:
myhostname = mx1.evilmail.pro
inet_interfaces = 203.0.113.10
inet_protocols = all
smtpd_tls_security_level = may
mynetworks = 127.0.0.0/8
syslog_name = postfix/in
Ключевой принцип: входящий инстанс не ходит наружу от своего IP для клиентских писем. Его задача — принять письмо на 25-й порт, положить в свою очередь и передать исходящему через loopback. PTR приёмного IP 203.0.113.10 указывает на mx1.evilmail.pro, и на этом его роль в деливерабилити заканчивается — жалобы и блэклисты его не касаются.
Исходящий инстанс: свой egress-IP, DKIM и без публичного smtpd
Здесь начинается вся тонкость деливерабилити. /etc/postfix-out/main.cf:
smtp_bind_address жёстко прибивает исходящие соединения к egress-IP 203.0.113.25. Без этого ядро выберет IP по маршруту, и письмо уйдёт с непредсказуемого адреса.
smtp_helo_name = out.evilmail.proобязан совпадать с PTR egress-IP. Это первое, что проверяют крупные провайдеры.
master_service_disable = smtp.inet гасит штатный слушатель 25-го порта — сервис smtp типа inet, который postmulti create копирует в master.cf. Указывать голое inet нельзя: это выключило бы все inet-сервисы, включая loopback-слушатель ниже. А smtp.inet бьёт точечно по имени сервиса — приём с loopback уцелеет, у него другое имя.
smtpd_milters вешает OpenDKIM только сюда. Входящий инстанс не подписывает — это принципиально.
Приём для реинжекта поднимаем в /etc/postfix-out/master.cf отдельным слушателем на loopback:
127.0.0.1:10026 inet n - n - - smtpd
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_milters=inet:127.0.0.1:8891
Этот слушатель принимает письма только с 127.0.0.1 — от входящего инстанса и от приложений. Наружу он невидим, а его имя сервиса (127.0.0.1:10026) отличается от погашенного smtp, поэтому master_service_disable его не трогает.
Сшиваем поток: как письмо переходит из in в out
Есть три рабочих способа передать письмо из приёмного инстанса в отправляющий.
1.content_filter / after-queue reinject. Во входящем main.cf или через транспорт письмо уходит на relay:[127.0.0.1]:10026. Скобки [...] отключают MX-резолвинг — идём прямо на loopback.
2.sender_dependent_default_transport_maps. Маршрутизация по отправителю: письма от рассыльных доменов уходят в out, остальное остаётся локальным. Гибко, когда нужен избирательный egress.
3.Приложение напрямую. Веб-бэкенд evilmail и temp-mail шлют письма прямо в 127.0.0.1:10026, минуя приём полностью. Транзакционка не проходит через MX-очередь вообще.
Loopback-инжект безопаснее внешнего relay: трафик не покидает хост, не нужен TLS между инстансами, а permit_mynetworks,reject гарантирует, что чужой на этот порт не зайдёт.
DNS и репутация под два IP
Всё разделение теряет смысл без правильного DNS. Конкретные записи для двух IP:
; A / AAAA
mx1.evilmail.pro. IN A 203.0.113.10
out.evilmail.pro. IN A 203.0.113.25
; MX домена → только приёмный IP
evilmail.pro. IN MX 10 mx1.evilmail.pro.
; PTR (в зоне провайдера IP)
10.113.0.203.in-addr.arpa. IN PTR mx1.evilmail.pro.
25.113.0.203.in-addr.arpa. IN PTR out.evilmail.pro.
; SPF — только egress-IP, приёмный НЕ перечисляем
evilmail.pro. IN TXT "v=spf1 ip4:203.0.113.25 -all"
; DKIM — селектор подписывается на исходящем
out._domainkey.evilmail.pro. IN TXT "v=DKIM1; k=rsa; p=MIGf..."
Логика такая: MX ведёт на mx1 (приём), а весь исходящий поток авторизован SPF только через 203.0.113.25. PTR исходящего IP совпадает с smtp_helo_name — это критично, крупные провайдеры режут доверие при расхождении forward/reverse. Приёмный IP в SPF не фигурирует вообще: он ничего не отправляет, значит и авторизовывать нечего. Результат — спам-жалобы, попадания в блэклисты и проблемы прогрева бьют исключительно по 203.0.113.25, а приём вашей клиентской почты по MX остаётся с незапятнанной репутацией.
Управление, логи и мониторинг
Повседневная эксплуатация идёт через postmulti и флаг -c у утилит очереди:
bash
# управление группой целиком
postmulti -g postout -p reload
postmulti -g postout -p stop
# всё разом поднять/остановить с default-инстанса
postfix start
# очередь конкретного инстанса
postqueue -c /etc/postfix-out -p
postqueue -c /etc/postfix-out -f # флаш deferred
# что вообще запущено
postmulti -l
Логи разделяем по syslog_name. Правило rsyslog разводит postfix/in и postfix/out в отдельные файлы:
if $programname startswith 'postfix/out' then /var/log/mail-out.log
& stop
if $programname startswith 'postfix/in' then /var/log/mail-in.log
& stop
В Postfix 3.4+ можно задать maillog_file per-instance и обойтись без rsyslog-фильтра. На исходящем ставьте отдельный алерт на длину deferred: при прогреве IP растущий deferred — первый сигнал, что вы упёрлись в лимиты принимающей стороны.
Чеклист перед продакшеном
PTR обоих IP совпадают с myhostname / smtp_helo_name соответствующего инстанса.
SPF содержит только egress-IP 203.0.113.25, приёмный не перечислен.
master_service_disable = smtp.inet на исходящем — штатный порт-25 smtpd погашен, наружу инстанс не слушает.
DKIM-milter (smtpd_milters) висит только на postfix-out.
smtp_bind_address и smtp_bind_address6 заданы явно.
Внешняя проверка HELO/PTR: openssl s_client -starttls smtp -connect out.evilmail.pro:25 — баннер и сертификат должны отдавать out.evilmail.pro.
Ни второго сервера, ни контейнеров, ни сторонних зависимостей — только штатный postmulti, который в ядре Postfix живёт с 2.6 и в актуальных 3.9/3.10 работает ровно так же. Один apt upgrade обновляет оба инстанса разом, потому что бинарники общие. А ночная рассылка на 40 000 писем теперь забивает свою очередь — и клиентская почта на MX этого даже не замечает.