Postscreen в Postfix: как отсечь спам-ботов до приёма письма
Postscreen отбраковывает ботнет-зомби ещё на TCP-рукопожатии, до очереди и content-фильтра. Разбираем взвешенные DNSBL, pregreet-тест, собственный резолвер и безопасное внедрение через режим лога.
EvilMail Team9 июля 2026 г.12 мин чтения
Один процесс smtpd на медленном боте — это удерживаемый слот, десятки мегабайт RAM и полный путь письма до content-фильтра, прежде чем вы вообще решите его выбросить. Боту это ничего не стоит: он ворованный, их у ботнета сотни тысяч, и ему всё равно, сколько ваших ресурсов он сожжёт. Асимметрия не в вашу пользу.
postscreen ломает эту асимметрию. Он стоит перед smtpd, обрабатывает входящее соединение в одном процессе, гоняет дешёвые проверки на этапе рукопожатия и передаёт в smtpd только тех, кто их прошёл. Ботнет-зомби до реального SMTP-движка не доходит вообще.
Сразу главная оговорка, на которой обжигаются чаще всего: postscreen живёт только на порту 25 — на inbound MX. Никогда на 587 и 465. Pregreet-тест задерживает баннер и штрафует того, кто заговорил раньше времени, — а нормальные почтовые клиенты на submission именно так и делают. Поставите postscreen на 587 — выкосите собственных аутентифицированных пользователей.
Проблема: smtpd дорогой, ботнет дешёвый
Гоняя это на тысячах доменов, мы видим стабильную картину: около 80% входящего мусора приходит от заражённых хостов, которые ведут себя одинаково глупо. Они не дожидаются баннера, шлют команды пачкой в одном пакете, светятся сразу в нескольких DNSBL. Это не таргетированные атаки, а тупые скрипты — и ловятся они такими же тупыми дешёвыми тестами.
Ключевая мысль всей статьи: не надо резать бинарно по одному чёрному списку. Один DNSBL иногда ошибается и цепляет чужой IP из того же /24. А взвешенная сумма нескольких списков плюс белые списки с отрицательным весом — устойчива. Именно так postscreen и устроен, и именно поэтому его правильная настройка почти не даёт ложных отказов.
Как это включается в master.cf
Перестановка простая: строку smtp inet теперь обслуживает postscreen, а сам smtpd переезжает на служебную строку smtpd pass, куда postscreen передаёт уже отфильтрованные соединения. Дополнительно нужны два помощника: dnsblog (асинхронный опрос DNSBL) и tlsproxy (postscreen сам TLS не терминирует, STARTTLS проксируется через него).
text
# /etc/postfix/master.cf
# ========== service type private unpriv chroot wakeup maxproc command
smtp inet n - y - 1 postscreen
smtpd pass - - y - - smtpd
dnsblog unix - - y - 0 dnsblog
tlsproxy unix - - y - 0 tlsproxy
# submission НЕ ТРОГАЕМ — здесь никакого postscreen
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
Обратите внимание на maxproc=1 у postscreen: он и должен быть один — это лёгкий мультиплексор соединений, а не пул воркеров. Проверить, что Postfix увидел раскладку:
Уровень 1 — permanent white/blacklist по IP. Работает мгновенно, до всякого DNS, читается из postscreen_access_list. Сюда попадают ваши smarthost'ы, мониторинги и заведомо злые сети.
Уровень 2 — pregreet + DNSBL. Недеструктивные тесты: соединение уже открыто, письмо в принципе можно принять в этом же сеансе. Именно здесь работает взвешенный скоринг.
Уровень 3 — глубокие протокол-тесты (pipelining, non-SMTP command, bare newline). Деструктивные: чтобы их проверить, надо разорвать соединение и заставить клиента переподключиться. Поэтому по умолчанию гоняются только для новых клиентов.
Чтобы не гонять DNSBL и не мучить хорошего отправителя на каждом письме, postscreen держит временный whitelist-кэш: прошёл проверки — на несколько часов попал в кэш, следующие соединения с этого IP летят в smtpd напрямую. Кэш обязательно на диске, btree, иначе после рестарта вы устроите повторный шторм проверок и словите rate-limit от DNSBL-провайдера.
Pregreet: ловим тех, кто говорит раньше баннера
RFC 5321 прямо требует: клиент обязан дождаться полного 220-приветствия, прежде чем что-либо отправлять. Нормальный MTA так и делает. Ботнет-скрипт — нет: он вываливает EHLO/HELO сразу, не читая ответа, чтобы сэкономить время.
postscreen на этом играет: он задерживает баннер на postscreen_greet_wait (обычно ~6 секунд) и шлёт многострочный 220. Если за время задержки в сокете уже лежат байты от клиента — это PREGREET, клиент заговорил вне очереди. Тест не требует ни DNS, ни списков, ни репутации — он режет тупые скрипты бесплатно и очень эффективно.
enforce означает: провалившего pregreet до smtpd не пускаем и даём 550. Конструкция ${stress?...} автоматически ужимает задержку под нагрузкой, чтобы очередь соединений не разбухала во время всплеска.
DNSBL с весами: не бинарно, а по сумме баллов
Это ядро настройки. В postscreen_dnsbl_sites каждый список получает вес через *N. Ответы суммируются, и если сумма достигает postscreen_dnsbl_threshold — соединение штрафуется. Белые списки (DNSWL) идут с отрицательными весами и гасят ложные срабатывания.
Разберём логику весов. Попадание в zen.spamhaus.org весит 3 — само по себе достигает порога и режет. Barracuda и SpamCop весят по 2: поодиночке не хватает, но вдвоём дают 4 и уверенно перешагивают порог. Это и есть устойчивость — чтобы отбить хоста, нужно согласие как минимум двух источников (или одного самого надёжного).
DNSWL с отрицательными весами делает обратное. list.dnswl.org отвечает разными кодами в зависимости от доверия к отправителю: чем выше доверие, тем более отрицательный вес (-2, -3, -4). Крупная легитимная рассылка, засветившаяся в DNSWL, уходит в минус и проходит, даже если случайно зацепилась за какой-то серый список. А postscreen_dnsbl_whitelist_threshold = -1 дополнительно кладёт такого отправителя во временный whitelist-кэш, чтобы больше его не теребить.
Почему порог 3, а не 1? Порог 1 превращает конструкцию обратно в бинарную «любой список = отказ» и наследует все ложные срабатывания худшего из списков. Порог 3 требует консенсуса и оставляет запас, чтобы отрицательные веса могли сработать.
Собственный резолвер — не опция, а требование 2026
Здесь самый частый провал внедрения, который мы разбираем на чужих серверах. Spamhaus, Barracuda и SpamCop блокируют запросы через публичные резолверы (8.8.8.8, 1.1.1.1, Quad9) и через крупные датацентр-резолверы. Их бизнес-модель — платный datafeed для больших объёмов, а бесплатный публичный DNS-путь для них просто дорогой абьюз.
Симптом провала коварный: DNSBL молча перестаёт срабатывать. Вместо ответа 127.0.0.x вы получаете 127.255.255.254 (запросы заблокированы) или пустоту — и postscreen честно считает, что хост чист. Ошибок в логе нет, письма идут, а весь ботнет проходит насквозь. Люди месяцами живут с выключенным по факту DNSBL и не замечают.
Лечение — локальный рекурсивный резолвер (unbound или BIND), который ходит в интернет с IP вашего MX, а не через провайдерский форвардер:
bash
# unbound: НЕ форвардить на публичные DNS, резолвить самому
# /etc/unbound/unbound.conf.d/resolver.conf
server:
interface: 127.0.0.1
do-ip6: no
# НИКАКИХ forward-zone на 8.8.8.8 / 1.1.1.1
Проверка обязательна перед каждым запуском в бой. Запрос к zen по тестовому IP 127.0.0.2 (в имени он записывается в обратном порядке) должен вернуть код листинга:
bash
dig +short 2.0.0.127.zen.spamhaus.org @127.0.0.1
# ожидаем 127.0.0.2 ... 127.0.0.11 — список работает
# получили 127.255.255.254 — вы заблокированы, DNSBL не работает
Для действительно больших объёмов (десятки миллионов запросов в сутки) публичный DNS-путь не годится в принципе — берётся rsync/datafeed-зона Spamhaus и разворачивается локально. Но для одного-двух MX корректно настроенного локального unbound достаточно.
Глубокие протокол-тесты
Три деструктивных теста ловят ботов, которые торопятся нарушить протокол:
pipelining — клиент шлёт команды пачкой, не дожидаясь ответов, там где RFC этого не разрешает. enforce — штрафуем.
non-SMTP command — в сокет прилетает бинарь или мусор вместо SMTP-команды. Это почти гарантированно вредоносный сканер, поэтому drop — рвём соединение молча.
bare newline — LF без CR. Тут держим ignore первое время: тест иногда цепляет старые, но легитимные MTA, и рубить его сразу рискованно.
Все три деструктивны: клиент получает разрыв и обязан переподключиться, после чего проходит уже как «старый». Для ботов это конец истории — они не переподключаются с чистым протоколом. Для легитимных отправителей разрыв случается один раз, дальше их держит whitelist-кэш.
Безопасное внедрение: сначала лог, потом топор
Правило, которое мы соблюдаем на каждом домене без исключений: postscreen не включается в enforce с первого дня. Иначе вы гарантированно отобьёте пару партнёров или собственный мониторинг и узнаете об этом из тикетов.
Шаг 1. Все *_action ставим в ignore. postscreen проводит все тесты и пишет вердикт в лог, но никого не отбивает. Почта идёт как раньше.
Шаг 2. Неделю читаем mail.log. Считаем, кого бы отбили и по какой причине.
Шаг 4. Всё, что должно проходить всегда — свои smarthost'ы, транзакционные шлюзы, мониторинги, крупные партнёры — вносим в permanent whitelist до перевода в enforce:
Шаг 5. Поднимаем action до enforceпо одному тесту, а не все разом. Сначала pregreet, посмотрели сутки — потом DNSBL, потом протокол-тесты.
Вот как выглядят реальные строки лога, которые вы будете читать:
text
postscreen[2011]: CONNECT from [203.0.113.5]:41234 to [203.0.113.10]:25
postscreen[2011]: PREGREET 11 after 0.08 from [203.0.113.5]: EHLO x\r\n
postscreen[2011]: DNSBL rank 5 for [203.0.113.5]
postscreen[2011]: NOQUEUE: reject: RCPT from [203.0.113.5]: 550 5.7.1
Service unavailable; client [203.0.113.5] blocked using zen.spamhaus.org
postscreen[2011]: PASS NEW [198.51.100.7]:52001
postscreen[2011]: PASS OLD [198.51.100.7]:52140
postscreen[2011]: WHITELISTED [203.0.113.10]:9033
PREGREET 11 after 0.08 — бот выпалил 11 байт через 0,08 секунды после connect, не дождавшись баннера. DNSBL rank 5 — сумма весов достигла 5 при пороге 3. PASS NEW — новый чистый клиент, положен в кэш; следующее его соединение уже PASS OLD. WHITELISTED — сработал permanent access-лист.
Чек-лист перед enforce
Локальный резолвер (unbound/BIND) работает и виден DNSBL — dig на тестовый IP возвращает 127.0.0.x, а не 127.255.255.254.
postscreen стоит только на порту 25, submission (587/465) не тронут.
mynetworks, smarthost'ы, мониторинги и партнёры внесены в postscreen_access.cidr со permit.
DNSWL с отрицательными весами включён, postscreen_dnsbl_whitelist_threshold = -1.
postscreen_cache_map на диске, тип btree — кэш переживает рестарт.
Отработана неделя в режиме ignore, лог прочитан, ложных кандидатов на отказ нет.
Настроен алерт на аномальный рост reject — резкий скачок обычно значит, что упал резолвер или DNSBL-провайдер поменял формат ответа.
После перехода в enforce мониторинг не выключаем: доля WHITELISTED против reject — ваш индикатор здоровья. Если reject внезапно пополз вверх без видимого всплеска ботнета, это почти всегда ложные отказы, и разбираться надо в тот же час, пока легитимная почта не начала копиться у отправителей.