Milter в Postfix без боли: порядок вызова, macros и совмещение rspamd с opendkim/opendmarc
Задвоенные заголовки, потерянные DKIM-подписи, письма в deferred и rspamd, который «не видит» вердикт opendmarc — всё это растёт из двух вещей: неправильного порядка милтеров и непонимания macros. Разбираем milter-конвейер как конвейер: кто за кем стоит, кто чей результат читает и что происходит при таймауте.
EvilMail Team22 июля 2026 г.11 мин чтения
Если у вас в почте творится что-то из этого списка — два заголовка DKIM-Signature в одном письме, dmarc=none при живом opendmarc, письма от cron уходят без подписи, а после перезапуска демона вся входящая падает в deferred — не спешите крутить настройки rspamd. Корень один: вы относитесь к милтерам как к набору независимых «плагинов», а это конвейер, где каждый следующий читает результат предыдущего. Порядок и macros решают всё.
Milter — это протокол, а не хук
Milter (Mail Filter API) родом из Sendmail. Postfix не «встраивает» фильтры внутрь себя — он реализует клиентскую сторону этого протокола. smtpd и cleanup выступают как MTA-клиент, а каждый демон-милтер — как отдельный сетевой сервер, слушающий unix- или inet-сокет. На каждой фазе SMTP-диалога Postfix открывает соединение и спрашивает милтер: что делать дальше.
Фазы идут строго по ходу диалога: CONNECT
Интеграция milter в Postfix: порядок, macros, rspamd + opendkim/opendmarc — EvilMail Blog
→
HELO
→
MAIL FROM
→
RCPT TO
→
DATA
→ передача заголовков → передача тела →
EOM
(end-of-message). На любой из них милтер может ответить
continue
,
reject
,
tempfail
,
quarantine
или
accept
. А вот менять само письмо — добавлять и переписывать заголовки, заменять тело — милтер может
только на фазе EOM
.
Отсюда первый неочевидный факт: если два милтера хотят вписать заголовок, они делают это на EOM в том порядке, в котором перечислены. Два DKIM-подписанта = два DKIM-Signature. Милтер, «переписывающий» Authentication-Results, затрёт вписанное предыдущим, если вы перепутали порядок. Milter — это не «событие», это позиция в очереди.
Два класса милтеров: smtpd_milters и non_smtpd_milters
Postfix различает два входа для почты, и у каждого свой список милтеров.
`smtpd_milters` обрабатывает письма, пришедшие по SMTP — порт 25 (входящая) и submission 587 (от ваших клиентов).
`non_smtpd_milters` обрабатывает письма, впрыснутые локально: sendmail(1), pickup — то есть всё от cron, от PHP mail(), от серверных скриптов. Эти письма идут через cleanup, минуя smtpd.
Классическая ошибка: DKIM-подпись повесили только на smtpd_milters. В итоге входящая почта проверяется, письма клиентов подписываются, а отчёты от cron и уведомления приложений уходят без подписи — и получатели их режут. Лечится одной строкой:
non_smtpd_milters = $smtpd_milters
Но с оговоркой: rspamd в non_smtpd_milters обычно не нужен. Если письмо уже прошло сканирование во входящем smtpd, повторный прогон на локальном впрыске — лишняя нагрузка и риск двойного скоринга. Для локальной почты хватает одного opendkim:
milter_protocol = 6 — значение по умолчанию начиная с Postfix 2.6, трогать не нужно. Оно включает шестую версию протокола: реджекты на RCPT, macros на каждой фазе. Понижать до 2 приходится только ради совсем древних милтеров, которых вы в 2026-м не встретите.
Порядок вызова: почему opendkim → opendmarc → rspamd
Милтеры вызываются строго слева направо, в порядке перечисления. Это не стилистика — это направление потока данных.
opendkim проверяет и подписывает DKIM и на EOM вписывает заголовок Authentication-Results с результатом dkim=pass/fail.
opendmarc не считает DKIM сам — он читаетAuthentication-Results, проверяет выравнивание доменов DKIM и SPF с From:, применяет политику из _dmarc и дописывает dmarc=pass/fail. Значит opendkim обязан отработать раньше и успеть вписать A-R.
rspamd ставится последним осознанно. Он видит символы DKIM_ALLOW/DKIM_REJECT, DMARC_POLICY_ALLOW/DMARC_POLICY_REJECT, R_SPF_ALLOW, читает готовый A-R и принимает итоговое решение: reject, greylist, add header, rewrite subject.
Переставьте opendmarc перед opendkim — и DMARC молча сломается. Ошибки в логах не будет: opendmarc просто не найдёт Authentication-Results (его ещё никто не вписал) и проставит dmarc=none всем подряд. Заметите вы это не сразу, а когда получатели начнут жаловаться на спам-папку для легитимной почты.
Macros: что доступно и где это настраивается
Milter-протокол передаёт демону не только сырой SMTP, но и набор macros — переменных Postfix — причём по фазам. Списки задаются отдельно для каждой фазы:
Реально важные macros для opendmarc и rspamd: {i} (queue-id, чтобы сшить логи), {client_addr} (IP отправителя для SPF и репутации), {auth_authen} и {auth_type} (логин и метод SASL-аутентификации), {cert_subject} (TLS-клиент), {daemon_name}.
Самый частый практический кейс — заставить rspamd отличать аутентифицированных отправителей, чтобы применять к ним отдельный набор правил: не штрафовать за SPF/DKIM, но следить за компрометацией аккаунта. Для этого {auth_authen} должен дойти до милтера на фазе MAIL FROM:
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} {auth_type}
Без {auth_authen} в этом списке rspamd не увидит логин на нужной фазе и будет считать все письма анонимными — со всеми вытекающими для скоринга submission-трафика.
Обработка отказов: milter_default_action и таймауты
milter_default_action определяет, что делает Postfix, когда милтер недоступен или молчит:
`tempfail` — отдать 4xx, отправитель повторит позже. Правильный выбор для прода: лучше задержать письмо, чем пропустить непроверенным.
`accept` — пропустить письмо без фильтрации. Выбор на этапе внедрения: если вы только поднимаете rspamd и он падает, почта не должна вставать колом.
`reject` — жёсткий 5xx, письмо теряется. Почти никогда не нужно.
`quarantine` — в hold-очередь.
Таймауты держат конвейер живым:
milter_connect_timeout = 30s — на установку соединения с сокетом.
milter_command_timeout = 30s — на ответ по каждой SMTP-команде.
milter_content_timeout = 300s — на обработку тела. Он намеренно большой: rspamd сканирует крупные вложения, распаковывает архивы, дёргает внешние проверки, и 30 секунд ему на письмо с вложением в 20 МБ может не хватить.
Реальный инцидент, ради которого всё это стоит понимать: opendkim повис — не упал, а именно завис на сокете, — при milter_default_action = tempfail. Каждое входящее письмо ждёт 30 секунд таймаута, получает 4xx, уходит в deferred. Через десять минут очередь забита, отправители копят повторы, а systemctl status opendkim показывает active (running) — процесс-то жив. Вывод: мониторить нужно сокеты, а не только процессы. Проверка доступности сокета ловит такое, а is-active — нет.
self_scan = yes означает, что прокси-воркер сам сканирует письмо, без отдельного normal-воркера. Для одного сервера этого достаточно, и это проще классической схемы proxy → normal.
Дальше есть два сценария архитектуры.
Сценарий 1 — три демона. opendkim и opendmarc живут отдельно и делают DKIM/DMARC, а rspamd занимается только скорингом и спам-фильтрацией. Так исторически устроено большинство инсталляций.
Сценарий 2 — rspamd делает всё. Модуль dkim_signing подписывает исходящую почту, модуль dmarc проверяет входящую, модуль arc добавляет ARC-подписи для форвардинга. opendkim и opendmarc выкидываются полностью.
Моя позиция на 2026 год однозначна: на новых инсталляциях отдавайте DKIM, DMARC и ARC целиком rspamd. Причины прагматичные — меньше сокетов в конвейере (один вместо трёх), один конфиг вместо трёх, и ARC работает из коробки, тогда как связка opendkim + opendmarc его без плясок не даёт. Строка smtpd_milters схлопывается до одного сокета, а точек отказа становится втрое меньше.
Когда opendkim всё же стоит держать отдельно: legacy-совместимость с уже работающей схемой, которую страшно трогать; отдельная команда, управляющая DKIM-ключами через свой пайплайн; регуляторные требования на изоляцию функции подписи. Во всех остальных случаях три демона — это технический долг, который вы тащите по инерции.
DKIM-подпись через dkim_signing требует, разумеется, опубликованного ключа. Напомню контекст DNS, без которого весь конвейер бессмысленен:
Диагностика: как увидеть, что милтер реально отработал
Первым делом откройте исходник полученного письма и прочитайте Authentication-Results. Там должны быть dkim=pass, spf=pass, dmarc=pass. Если dmarc=none при живой _dmarc-записи — почти наверняка перепутан порядок милтеров.
Что письмо прошло именно rspamd — видно по заголовку X-Spamd-Result с разложением по символам и по наличию DKIM-Signature у исходящих. Если ловите milter timeout, первым делом проверьте, что сокет вообще принимает соединения — это тот самый случай «процесс жив, а сокет молчит». Простейший тест доступности сокета, который и стоит вынести в мониторинг:
Если соединение не устанавливается, поднимайте уровень логирования у конкретного демона (LogLevel у opendkim, debug_modules у rspamd) и смотрите, на какой фазе он спотыкается.
Чеклист внедрения
Порядок в smtpd_milters строго opendkim → opendmarc → rspamd.
non_smtpd_milters = unix:/run/opendkim/opendkim.sock — чтобы локальная почта подписывалась.
milter_protocol = 6 (дефолт, не трогать без причины).
milter_default_action = accept на внедрении, переключить на tempfail на проде.
Таймауты: connect/command 30s, content 300s (rspamd + большие вложения).
milter_mail_macros содержит {auth_authen} и {auth_type} для различения submission-трафика.
Мониторинг доступности сокетов, а не только systemctl is-active.
Тестовая отправка на верификатор (check-auth / mail-tester) и проверка Authentication-Results.
ARC настроен, если сервер форвардит почту — иначе форварды ломают DMARC.
План отката: закомментированная предыдущая строка smtpd_milters и postfix reload наготове.
Milter-конвейер прощает многое, кроме сборки вслепую. Соберите цепочку осознанно: opendkim пишет вердикт, opendmarc его читает, rspamd читает оба и решает. Всё остальное — таймауты, macros, default_action — обслуживает эту единственную линию потока данных.