Защита от backscatter в Postfix: отклонять на RCPT, а не отлупать письмом
Backscatter — это спам, который рассылаете вы сами: приняли письмо на несуществующий адрес, потом сгенерировали DSN на подделанный envelope-from и ударили по невиновной жертве. Разбираем, где именно в Postfix утекает accept-then-bounce и как закрыть это ответом 550 прямо в SMTP-диалоге.
EvilMail Team23 июля 2026 г.11 мин чтения
Открываешь mailq на почтовом сервере и видишь несколько сотен писем от MAILER-DAEMON в статусе deferred. Все — отлупы. Получатели в них — люди и домены, которых ты в глаза не видел: [email protected], [email protected], [email protected]. Ни одного из этих писем ты не отправлял. А через день прилетает первый reject от Gmail с формулировкой про репутацию, и проверка показывает, что твой IP уже в Backscatterer.org.
Диагноз в одно предложение: сервер принял на RCPT TO то, что не смог доставить, и отлуп улетел подделанной жертве. Это backscatter — и это не проблема фильтрации входящего спама. Это проблема того, что твой сервер сам стал источником спама.
Что такое backscatter и почему это репутационная катастрофа
Backscatter в Postfix: отклонение на RCPT вместо генерации отлупов — EvilMail Blog
Механика простая и целиком лежит в SMTP. Спамер отправляет письмо на несуществующий адрес твоего домена, но в
MAIL FROM
(envelope-from) вписывает не свой адрес, а адрес жертвы —
, принимает письмо в очередь, а потом выясняет, что доставить некуда: пользователя нет. RFC 5321 обязывает уведомить отправителя о невозможности доставки — и Postfix честно генерирует DSN (delivery status notification). Только «отправитель» здесь — подделанный адрес жертвы. Отлуп летит человеку, который вообще ни при чём.
Умножь это на объём спам-рассылки, и твой сервер начинает бомбардировать тысячи невиновных ящиков сообщениями MAILER-DAEMON. Последствия:
Листинг в Backscatterer.org — специализированном чёрном списке именно для источников отлупов (обслуживается проектом UCEPROTECT).
Рост объёма исходящих bounce, который забивает очередь и жрёт ресурсы.
Деградация репутации у Gmail и Microsoft: они видят поток DSN с твоего IP на адреса, которые никогда с тобой не переписывались, и понижают доверие.
Ключевая мысль, вокруг которой строится вся защита: вопрос не в том, «как отфильтровать отлупы», а в том, «как вообще их не создавать».
Первопричина: валидация получателя ПОСЛЕ приёма письма
Паттерн, который убивает репутацию, называется accept-then-bounce. Postfix отдаёт 250 на RCPT TO, ставит письмо в очередь, а невозможность доставки выясняется уже на этапе локальной доставки — unknown user, over quota, недоступный backend. К этому моменту SMTP-соединение с отправителем давно закрыто, и единственный способ сообщить о провале — сгенерировать новое письмо на forged sender.
Типовые дыры, через которые Postfix соглашается принять недоставляемое:
Пустой local_recipient_maps = — принимает всё для доменов из mydestination.
Catch-all алиас вида @domain -> admin — любой адрес считается валидным на приёме.
Резервный MX с relay_domains, но без relay_recipient_maps — принимает любой адрес для домена и надеется, что разберётся primary.
Кривой SQL в virtual_mailbox_maps, который возвращает непустую строку на любой запрос.
Любого одного пункта достаточно, чтобы превратить сервер в backscatter-пушку. Вот как выглядят оба пути одного и того же письма:
Разница в том, кто генерирует отлуп. Ответил 550 прямо на RCPT TO — за уведомление отправителя отвечает его сервер: он получил permanent reject внутри живого соединения. Ты не создаёшь и не отправляешь ничего.
Отклонять на этапе RCPT: базовая конфигурация
Для локальных доменов (тех, что в mydestination) Postfix умеет проверять получателя прямо в SMTP-диалоге. По умолчанию smtpd_reject_unlisted_recipient = yes, но это стоит задать явно и добавить reject_unlisted_recipient в список рестрикций, чтобы поведение не зависело от дефолтов конкретной версии.
Критичный момент — все reject-коды должны быть 550 (permanent), а не 450/451 (temporary). Дефолт для этих параметров в Postfix — как раз 550, но во многих сетапах их случайно роняют в 450 «чтобы не терять почту при обслуживании». Это ошибка: временный код заставляет спамера ретраить бесконечно, а легитимный сервер держит письмо в очереди несколько дней зря. Для несуществующего адреса ответ всегда должен быть окончательным.
Порядок рестрикций тоже не случаен. reject_unauth_destination идёт раньшеreject_unlisted_recipient: сначала отсекаем попытки использовать нас как открытый релей (чужой домен, который мы не обслуживаем), и только потом проверяем существование получателя в своих доменах. Правильный ответ в логе выглядит так:
550 5.1.1 <[email protected]>: Recipient address rejected:
User unknown in local recipient table
Виртуальные домены: точная проверка вместо catch-all
Если ящики живут в MySQL через virtual_mailbox_maps, вся защита сводится к одному правилу: запрос должен возвращать строку только для реально существующего активного адреса.
sql
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
Проверяется это без перезапуска сервера, прямым запросом к карте:
bash
# существующий адрес — вернёт "1"
postmap -q "[email protected]" mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
# несуществующий — пустой вывод = адрес будет отклонён на RCPT (правильно)
postmap -q "[email protected]" mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
Пустой вывод на втором запросе — это то, что нужно. Он означает, что при RCPT TO: [email protected] Postfix ответит 550, а не примет письмо.
Catch-all на приёме (@evilmail.pro -> ...) выглядит удобно, но по сути это «принять любой мусор и разбираться потом». В контексте evilmail.pro соблазн максимальный: temp-email — это тысячи эфемерных адресов, которые появляются и протухают ежеминутно, и хочется просто принимать всё подряд. Но именно поэтому проверка идёт напрямую в БД по каждому RCPT: живой адрес — принимаем, несуществующий или уже протухший — режем 550 в диалоге. Никакого catch-all, никакой очереди отлупов.
Вот где именно валидация утекает в разных типах назначения:
Резервный MX и релей — классическая ловушка
Backup MX — самое частое место утечки, потому что его настраивают «на всякий случай» и забывают. Схема отказа: резервный сервер принимает письмо на любой адрес домена (у него же relay_domains = evilmail.pro), потом пытается отдать его primary, primary режет unknown user — и backup генерирует backscatter, потому что письмо он уже принял.
Лечится это тем, что backup MX должен знать список получателей ровно так же, как primary:
Запомни как правило: backup MX без заполненного recipient map опаснее, чем полное отсутствие backup MX. Отсутствующий резерв просто заставит отправителя ретраить на primary. Дырявый резерв — активно рассылает отлупы за твой счёт.
reject_unverified_recipient: мощно, но с оговорками
Бывает, что вести список получателей нельзя — например, ты форвардер перед чужой инфраструктурой и не знаешь, какие адреса там живут. Тогда Postfix умеет проверять получателя callout-запросом: делает пробное соединение к целевому серверу и смотрит, примет ли тот RCPT.
Минусы честно, без прикрас. Probe-запросы сами по себе выглядят подозрительно и напоминают спамерскую разведку адресов — часть серверов отвечает на них 450 или молча режет. Upstream может рейт-лимитить твои проверки. И есть реальный риск самому попасть в списки за callout-активность. Поэтому это последнее средство, а не первое: сначала нормальные recipient maps, и только если их принципиально не собрать — верификация с кэшем и осторожными таймингами.
Защита от ВХОДЯЩЕГО backscatter
Обратная сторона медали: отлупы на письма, которые ты действительно отправил, приходят с null sender <> — и их принимать надо, иначе сломаешь легитимные DSN о своей же почте. Задача — отличить настоящий отлуп от поддельного.
Правильный инструмент — BATV (Bounce Address Tag Validation). Исходящие письма подписывают envelope-from тегом prvs=, привязанным к дате и HMAC-ключу. Настоящий отлуп вернётся именно на этот подписанный адрес, а поддельный придёт на голый [email protected] без тега — и его можно смело резать. Для форвардинга в паре с BATV идёт SRS (Sender Rewriting Scheme), чтобы не ломать SPF при пересылке.
Если BATV не внедрён, остаётся Postfix Backscatter Howto — набор header_checks/body_checks, которые ловят характерные признаки поддельных MAILER-DAEMON, плюс SPF-проверка на null sender. Это костыль последнего рубежа. И главное правило: нельзя блокировать `<>` целиком — так ты выкинешь легитимные уведомления о недоставке своих писем.
Диагностика: вы уже источник или ещё нет
Прежде чем что-то менять, надо понять, в каком ты состоянии — «принимаю и бунсю» или «режу на RCPT».
bash
# растущий счётчик = вы генерируете backscatter прямо сейчас
mailq | grep -c MAILER-DAEMON
# отлупы, где вы отправитель, а envelope-from пустой
grep 'status=bounced' /var/log/mail.log | grep 'from=<>'
# аудит карт и кодов: ищем пустые maps и не-550
postconf -n | grep -E 'recipient_maps|reject_code|reject_unlisted'
# логи в реальном времени (systemd-инстансы)
journalctl -u postfix@- --since '1 hour ago'
Решающий тест — прогнать swaks на заведомо несуществующий адрес и посмотреть на ответ в диалоге:
<** 550 5.1.1 <[email protected]>: Recipient address
rejected: User unknown
Если вместо этого приходит 250 Ok, а через минуту в логе появляется status=bounced — ты backscatter-источник, чини конфиг немедленно. И отдельно проверь свой IP на Backscatterer.org (ips.backscatterer.org): если уже листнут, после исправления конфига запрашивай делистинг.
Чеклист внедрения
Прогнать postconf -n и убедиться, что нет пустого local_recipient_maps =.
Проверить, что все unknown_*_reject_code равны 550, а не 450/451.
Убедиться, что reject_unlisted_recipient присутствует в smtpd_recipient_restrictions и стоит после reject_unauth_destination.
Убрать все catch-all алиасы и запросы virtual_mailbox_maps, которые возвращают строку для любого адреса.
На каждом backup MX заполнить и синхронизировать relay_recipient_maps со списком ящиков primary.
Прогнать swaks на заведомо несуществующий адрес — ждать 550 внутри диалога, а не 250 с последующим bounce.
Внедрить BATV (prvs=) и SRS против входящего backscatter; <> не блокировать целиком.
Поставить мониторинг: alert на рост mailq | grep -c MAILER-DAEMON и периодическую проверку IP в Backscatterer.org.