Защита SMTP AUTH на порту 587: rate limiting, задержки и fail2ban против перебора паролей
fail2ban ловит шумную атаку с одного IP — и полностью слепнет к медленному password spraying с сотен адресов. Разбираем защиту submission как конвейер из четырёх слоёв плюс отдельный контур per-account лимитов, с реальными строками лога и готовыми конфигами Postfix, Dovecot и nftables.
EvilMail Team24 июля 2026 г.13 мин чтения
Открываешь mail.log в три ночи, потому что мониторинг разбудил алертом по нагрузке, и видишь вот это:
UGFzc3dvcmQ6 — это base64 от Password:, то есть кто-то методично перебирает пароли к SASL AUTH. Вопрос не в том, атакуют ли вас — на любой публичный 587-й порт долбятся круглосуточно. Вопрос в том, какой именно перебор вы видите в логе и что из вашей защиты реально его останавливает.
25-й порт при этом остаётся тихим, и это не случайность. На 25-м нет
Защита SMTP AUTH и submission (порт 587) от брутфорса: Postfix, Dovecot, fail2ban — EvilMail Blog
AUTH
— там только MTA-to-MTA обмен, и
postscreen
отсекает ботнет ещё до
DATA
. Подбирать там нечего: нет замка — нет и ковыряния. А submission (587 STARTTLS и 465 implicit TLS) — это дверь с замком, за которой сидит
smtpd_sasl_auth_enable=yes
. Вот её и ковыряют.
И здесь начинается главная ошибка. Типовой совет звучит как «поставь fail2ban и спи спокойно». fail2ban отлично ловит первую строку из лога выше — сотни попыток с 45.148.10.213. Но он структурно слеп ко второй угрозе: медленному password spraying, где на каждый из сотен IP приходится 2-3 попытки, maxretry никогда не достигается, а суммарно по аккаунту sales@ прилетает несколько тысяч проверок за час. Одна затычка эту атаку не видит. Нужен конвейер.
Поверхность атаки: где именно висит замок
Разложим по портам, потому что защита у каждого своя:
25 (smtp) — приём почты от чужих MTA. AUTH выключен, работает postscreen, greylisting, RBL. Брутфорсу тут делать нечего.
587 (submission) — отправка почты вашими клиентами через STARTTLS. AUTH включён. Это основная мишень.
465 (submissions) — то же самое, но implicit TLS (шифрование с первого байта). Мишень аналогичная.
Ключевой момент, который многие упускают: `postscreen` не защищает 587 и 465. Он навешивается только на smtpd за 25-м портом. На submission приходит легитимный клиент с логином и паролем, и отличить его от подборщика по одному TCP-хендшейку нельзя — оба выглядят одинаково до момента AUTH.
Что видит атакер со своей стороны: открытый AUTH LOGIN/AUTH PLAIN, и — почти всегда — валидный список адресов. Их берут из утечек, из каталога через старый VRFY, из веб-сайта компании (info@, sales@, admin@). Логины известны, остаётся пароль. Вся оборона строится вокруг того, чтобы сделать проверку одного пароля дорогой по времени и заметной по объёму.
Слой 1: не давать подбирать без TLS
Первое правило — AUTH вообще не должен быть доступен в открытом виде. Если атакер может слать AUTH LOGIN по нешифрованному каналу, он подбирает пароль и заодно читает чужие в открытую. В main.cf:
smtpd_tls_auth_only = yes
Это глобально запрещает AUTH до успешного STARTTLS. Дальше — убеждаемся, что на 25-м порту AUTH выключен полностью, а вся submission-логика вынесена в master.cf с собственными override. Вот боевой блок для 587:
smtpd_tls_security_level=encrypt делает шифрование обязательным — без STARTTLS клиент даже до AUTH не дойдёт. Это отсекает целый класс примитивных ботов, которые не умеют в TLS.
Слой 2: замедление на уровне Postfix — anvil и error-delay
smtpd_client_auth_rate_limit=10 — ключевая строка (доступна начиная с Postfix 3.6). Она ограничивает число попыток AUTH с одного клиента за окно anvil_rate_time_unit, которое я ставлю в main.cf:
anvil_rate_time_unit = 60s
Десять попыток аутентификации в минуту с одного IP — легитимному почтовому клиенту хватает с запасом, подборщику это рубит скорость на порядки. Когда лимит срабатывает, в логе появляется отметка anvil:
postfix/anvil[3312]: statistics: max auth rate 10/60s for (submission:45.148.10.213) at Jul 4 03:14:07
Второй механизм — штрафные задержки. smtpd_error_sleep_time=5s заставляет Postfix выдерживать паузу перед ответом после того, как клиент превысил smtpd_soft_error_limit, а smtpd_hard_error_limit определяет, после скольких ошибок соединение рвётся. smtpd_junk_command_limit=3 бьёт по тем, кто гоняет NOOP/RSET в цикле. Эффект арифметический: атака, которая при нулевых задержках выдавала бы ~10 000 попыток в минуту, превращается в сотни. Это не блокировка — это налог на скорость, и он ложится только на того, кто ошибается пачками.
Слой 3: задержки в Dovecot SASL
Postfix отдаёт проверку пароля Dovecot через SASL, и у Dovecot есть свой рычаг — задержка на неуспешную аутентификацию. В 10-auth.conf:
По умолчанию auth_failure_delay равен 2 секундам; я поднимаю до 5. Логика простая: легитимный клиент ошибается паролем раз в месяц и эти 5 секунд не заметит, а подборщик получает пять секунд принудительной паузы на каждый промах. Для последовательного перебора это экспонента боли за ноль денег.
Отдельно предупреждение про кэш. auth_cache ускоряет успешные логины, но с негативным кэшированием (auth_cache_negative_ttl) надо быть аккуратным: если закэшировать «неверный пароль», можно случайно ослабить задержку или огрести странное поведение при смене пароля. Держите негативный TTL коротким или нулевым. Промах пишется так:
Эта строка — то, что дальше будет разбирать fail2ban.
Слой 4: fail2ban ловит шумную атаку
Теперь ретроспективный слой. fail2ban читает лог, считает неудачи по IP и банит на файрволе. Джейл postfix-sasl уже умеет матчить и Postfix, и Dovecot строки. В jail.local:
Порог `maxretry`. Пять неудач за 10 минут — это компромисс. Опустите до 2-3 — и начнёте банить мобильных клиентов за CGNAT, где сотни абонентов оператора сидят за одним публичным IP, и один забывчивый пользователь утащит в бан весь диапазон. Поднимете до 20 — пропустите медленные атаки. Пять — рабочая середина для submission.
`banaction = nftables-multiport`. На больших банлистах iptables деградирует: каждый пакет линейно проходит по цепочке правил. nftables (и legacy-ipset) хранит адреса в named set с доступом за O(1) — тысячи забаненных IP не влияют на throughput. На проде с submission под постоянным огнём это не опция, а необходимость.
Джейл `recidive`. Он читает лог самого fail2ban и банит на неделю тех, кто уже попадал в бан несколько раз за сутки. Рецидивисты возвращаются — recidive избавляет от них надолго.
Перед боем фильтр надо прогнать по реальному логу:
Смотрите на Lines matched — если ноль, ваш syslog_name или формат лога не совпал с regex, и джейл будет молчать при живой атаке. Статус и ручной разбан:
bash
fail2ban-client status postfix-sasl
fail2ban-client set postfix-sasl unbanip 45.148.10.213
Слепая зона: распределённый password spraying
А теперь честно про то, чего fail2ban не может. Современная атака 2026 года выглядит не как «10 000 попыток с одного IP», а как «3 попытки с каждого из 2000 IP по аккаунту sales@». На каждый отдельный адрес приходится меньше maxretry, findtime истекает раньше, чем накопится порог — джейл не срабатывает ни разу. При этом аккаунт получает 6000 проверок пароля. fail2ban считает по IP, а атака размазана по IP. Ей нужен другой учёт — по имени пользователя, через все адреса сразу.
Визуальный вывод из матрицы: у строки spraying единственная галочка — это per-account контур. Реализуется двумя инструментами.
postfwd/postfwd2 — policy-демон перед Postfix, считающий события по произвольному ключу. Правило вида «не больше 3 неудачных AUTH на один sasl_username за час, дальше REJECT» ловит перебор по аккаунту независимо от того, с каких IP он идёт:
id=SASL_FAIL
sasl_username=~^(.+)$
action=rate(sasl_username/3/3600/REJECT too many auth failures)
Dovecot weakforced (wforce) — специализированный auth policy server от разработчиков Dovecot ровно под эту задачу. Он держит распределённое состояние по логинам, паролям и IP, умеет отдавать вердикт reject/tarpit и легко интегрируется с внешними репутационными базами вроде AbuseIPDB — засветившийся там IP можно отдавать сразу в banaction. Для нагруженного submission wforce — правильный ответ на spraying.
Проверка и наблюдаемость
Живую конфигурацию проверяем без перезапусков:
bash
postconf -M submission/inet
postconf smtpd_client_auth_rate_limit
doveadm auth test [email protected]
fail2ban-client status recidive
Кто именно долбится прямо сейчас — одной строкой по логу:
Если верхний IP выдаёт сотни попыток — это классика для fail2ban. Если же список длинный, а у каждого IP по 2-3 — вы смотрите на spraying, и здесь надо перегруппировать grep по sasl_username, а не по IP, чтобы увидеть настоящую цель.
Задержки и баны проверяются симуляцией через swaks:
Прогоните в цикле десяток раз и замерьте, как растут паузы между ответами и на какой попытке приходит бан — это единственный честный способ убедиться, что конвейер собран правильно, а не просто выглядит правильно в конфиге.
Чеклист внедрения
smtpd_tls_auth_only = yes — никакого AUTH без TLS, ни на одном порту.
AUTH полностью выключен на 25; вся аутентификация только на 587/465.
smtpd_client_auth_rate_limit=10 при anvil_rate_time_unit=60s — потолок попыток на клиента.
smtpd_error_sleep_time=5s плюс soft/hard error limits — штрафные задержки за мусорные команды.
auth_failure_delay = 5s в Dovecot, disable_plaintext_auth = yes, короткий негативный TTL кэша.
fail2ban джейл postfix-sasl с maxretry=5/findtime=10m и обязательно recidive.
banaction = nftables-multiport (или ipset) вместо plain iptables — O(1) на больших банлистах.
Per-account контур: postfwd-правило rate(sasl_username/3/3600/REJECT) или Dovecot wforce против spraying.
Ежедневный мониторинг топ-аккаунтов по числу неудач AUTH, а не только топ-IP.
Алерт на всплеск Password mismatch и еженедельная ревизия банлиста recidive.