Открываешь mailq на почтовом сервере и видишь несколько сотен писем от MAILER-DAEMON в статусе deferred. Все — отлупы. Получатели в них — люди и домены, которых ты в глаза не видел: [email protected], [email protected], [email protected]. Ни одного из этих писем ты не отправлял. А через день прилетает первый reject от Gmail с формулировкой про репутацию, и проверка показывает, что твой IP уже в Backscatterer.org.
Диагноз в одно предложение: сервер принял на RCPT TO то, что не смог доставить, и отлуп улетел подделанной жертве. Это backscatter — и это не проблема фильтрации входящего спама. Это проблема того, что твой сервер сам стал источником спама.
Что такое backscatter и почему это репутационная катастрофа
Механика простая и целиком лежит в SMTP. Спамер отправляет письмо на несуществующий адрес твоего домена, но в MAIL FROM (envelope-from) вписывает не свой адрес, а адрес жертвы — [email protected]. Твой сервер отвечает 250 OK на RCPT TO, принимает письмо в очередь, а потом выясняет, что доставить некуда: пользователя нет. 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 в список рестрикций, чтобы поведение не зависело от дефолтов конкретной версии.
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_unlisted_recipient,
permit
smtpd_reject_unlisted_recipient = yes
unknown_local_recipient_reject_code = 550
unknown_virtual_alias_reject_code = 550
unknown_virtual_mailbox_reject_code = 550
unknown_relay_recipient_reject_code = 550Критичный момент — все 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, вся защита сводится к одному правилу: запрос должен возвращать строку только для реально существующего активного адреса.
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1Проверяется это без перезапуска сервера, прямым запросом к карте:
# существующий адрес — вернёт "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:
relay_domains = evilmail.pro
relay_recipient_maps = hash:/etc/postfix/relay_recipients
unknown_relay_recipient_reject_code = 550Файл relay_recipients синхронизируется с primary — обычно скриптом выгрузки списка ящиков плюс rsync по cron, раз в несколько минут. Формат простой:
Запомни как правило: backup MX без заполненного recipient map опаснее, чем полное отсутствие backup MX. Отсутствующий резерв просто заставит отправителя ретраить на primary. Дырявый резерв — активно рассылает отлупы за твой счёт.
reject_unverified_recipient: мощно, но с оговорками
Бывает, что вести список получателей нельзя — например, ты форвардер перед чужой инфраструктурой и не знаешь, какие адреса там живут. Тогда Postfix умеет проверять получателя callout-запросом: делает пробное соединение к целевому серверу и смотрит, примет ли тот RCPT.
address_verify_map = btree:$data_directory/verify_cache
address_verify_negative_refresh_time = 3h
smtpd_recipient_restrictions =
...
reject_unverified_recipient
unverified_recipient_reject_code = 550Минусы честно, без прикрас. 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».
# растущий счётчик = вы генерируете 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 на заведомо несуществующий адрес и посмотреть на ответ в диалоге:
swaks --server mx.evilmail.pro \
--to [email protected] \
--from [email protected]Что должно быть:
<** 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


