03:14. Пейджер орёт. У пользователя sales@ увели пароль фишингом двумя днями раньше, и вот сейчас через submission на порту 587 с его SASL-логином за 20 минут ушло около 8000 писем на купленную базу. К утру IP в Spamhaus XBL и CSS, в Microsoft SNDS счётчик жалоб красный, Gmail Postmaster показывает spam rate под 8%. Пароль утёк давно — антивирус тут ни при чём. Вопрос не «как не пустить взлом», а «как сделать так, чтобы один слитый пароль не сжёг IP-пул, который прогревался полгода».
Сигнатурный антиспам на исходящем не спасает — он опаздывает: письма из скомпрометированного ящика технически валидны, у них правильный DKIM, проходящий SPF и аутентифицированный отправитель. Спасают три вещи, которые надо построить заранее: лимиты отправки по аккаунту, автоматическая блокировка по аномалии и физическая изоляция исходящего потока в отдельный IP-пул.
Как взлом выглядит на графиках, а не в логах
К моменту, когда что-то всплывёт в maillog, объём уже улетел. Ловить надо по поведению, а поведение видно на метриках задолго до того, как вы откроете лог грепать.
Рабочие сигналы аномалии:
- Всплеск messages/hour против базовой линии. Норма частного ящика — 20–50 писем в час, чаще меньше. Резкий скачок до сотен в час — это не «активный менеджер», это скрипт.
- Взрыв числа уникальных получателей. Живой человек пишет одним и тем же 15 контактам. Спам-прогон — сотни уникальных
RCPT TO, которых в истории переписки не было. - Доля 5xx-баунсов вверх, open rate в ноль. Купленная база гниёт: половина адресов не существует. Всплеск hard bounce при нулевых открытиях — почти диагноз.
- SASL-логин из новой ASN или гео. Юзер год ходил с домашнего провайдера в Стамбуле, а сейчас аутентифицируется из хостинг-ASN в другой стране.
- Отправка в нетипичное время. 8000 писем в 3 часа ночи по локальному времени владельца.
Ни один из этих сигналов не про содержимое письма — все про форму потока. Поэтому поведенческий детект срабатывает за минуты, а контентная фильтрация — постфактум, когда репутация уже потрачена.
Два рубежа: мягкий троттлинг и жёсткая блокировка
Первое архитектурное решение — разнести MSA (587/submission, аутентифицированная клиентская отправка) и MTA (25, входящий SMTP от чужих серверов). Лимиты и блокировки вешаются на submission, где есть SASL-логин и понятно, *кто* отправляет. На порту 25 отправителя нет — там только чужие MX.
Дальше — два рубежа, и нужны оба.
Рубеж 1 — soft limit. При превышении порога отдаём temp-fail 450 4.7.1. Легитимный почтовый клиент увидит временную ошибку и корректно повторит попытку позже — пользователь ничего не заметит. А спам-скрипт, который льёт в один поток и не разбирает 4xx, захлёбывается: он либо ретраит в лоб и упирается в тот же лимит, либо роняет письмо в собственный fail-лог. Мы выигрываем минуты, за которые сработает второй рубеж или дежурный.
Рубеж 2 — hard limit. Когда объём пробил жёсткий порог, SASL для аккаунта отключается, а его очередь встаёт на hold. Это уже необратимый до ручного разбора переход.
Почему нельзя обойтись одним рубежом. Только жёсткий дроп даёт ложные срабатывания: у клиента легитимная CRM-рассылка на 400 писем или менеджер разослал КП по отделу — снести это молча значит потерять почту и доверие. Только soft-defer тоже не спасает: если весь ответ на атаку — «отдавать 450», то за час скрипт всё равно продавит объём ретраями. Работает только связка: мягкий тормоз ловит и замедляет, жёсткий рубеж добивает.
Считаем письма по SASL-username, а не по IP
Главная инженерная ошибка, которую я постоянно вижу в чужих конфигах: лимит вешают на встроенный anvil в Postfix.
# Так делать НЕ надо для роуминговых пользователей:
smtpd_client_message_rate_limit = 100
smtpd_client_recipient_rate_limit = 200
anvil_rate_time_unit = 60sAnvil считает по клиентскому IP, и это ломается в обе стороны. За корпоративным NAT сотня легитимных пользователей выходит с одного адреса — и упирается в общий лимит, хотя каждый шлёт по десять писем. А злоумышленник с ботнета, наоборот, размазывает отправку по сотням IP и обходит порог полностью, потому что с каждого адреса писем мало. Ключ должен быть не IP, а тот, кто аутентифицировался: `sasl_username`.
Практично это делается через postfwd — policy-демон, который умеет лимитировать по любому атрибуту запроса:
id=RL-SOFT
sasl_username=~/.+/
action=rate($$sasl_username/100/3600/450 4.7.1 sending rate exceeded, retry later)
id=RL-HARD
sasl_username=~/.+/
action=rate($$sasl_username/300/3600/554 5.7.1 account suspended)Подключаем policy-сервис только на submission — через -o в master.cf, чтобы не трогать входящий трафик на порту 25:
# master.cf, сервис submission (587):
submission inet n - y - - smtpd
-o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,check_policy_service inet:127.0.0.1:10040,rejectПорт 10040 — дефолтный для postfwd. Альтернатива, если вы уже гоняете rspamd на исходящем, — его модуль ratelimit с бакетами по принципу leaky bucket. Он даёт разумный burst плюс устойчивую скорость:
# /etc/rspamd/local.d/ratelimit.conf
rates {
authenticated {
selector = 'user';
bucket = {
burst = 50;
rate = "100 / 1h";
}
}
}burst = 50 разрешает короткий всплеск (человек разослал накопившееся), а rate = "100 / 1h" держит средний потолок. Ключ selector = 'user' — это снова аутентифицированный пользователь, не IP. Пороги на массовом тарифе я держу консервативно: 100/час soft, 300/час hard. Транзакционным аккаунтам и подтверждённым рассылкам порог поднимается индивидуально — лимит должен быть свойством аккаунта, а не глобальной константой.
Автоблокировка и зачистка очереди
Порог пробит — дальше без человека. Policy-демон или rspamd дёргает hook-скрипт, который по порядку делает четыре вещи.
1. Отключает SASL — kill-switch. В схеме на Dovecot passdb это флаг в БД:
UPDATE virtual_users SET enabled = false WHERE email = '[email protected]';Одного флага мало — активная сессия может висеть открытой и продолжать лить. Рвём её:
doveadm kick [email protected]2. Ставит письма отправителя на hold. Не удаляет — именно hold. Postfix 3.1+ отдаёт очередь в JSON, что позволяет фильтровать по отправителю точечно:
# всё от скомпрометированного отправителя — на hold
postqueue -j | jq -r 'select(.sender=="[email protected]").queue_id' \
| postsuper -h -3. Точечно удаляет подтверждённый спам. Только после того, как глазами убедились, что это действительно рассылка, а не легитимная почта, попавшая под раздачу:
postqueue -j | jq -r 'select(.sender=="[email protected]").queue_id' \
| postsuper -d -Если инцидент массовый и надо заморозить вообще всё для разбора — postsuper -h ALL, потом разгребать.
4. Уведомляет. Владельцу ящика (сменить пароль, проверить устройства) и в саппорт с деталями инцидента.
Почему hold, а не delete по умолчанию. Удалённое письмо — это стёртая улика. При разборе инцидента содержимое очереди — доказательная база: что именно рассылалось, по какой базе, с какими заголовками. Сначала замораживаем и смотрим, потом удаляем. Обратный порядок необратим.
Изоляция IP: отдельные пулы для исходящего
Всё выше уменьшает ущерб, но не отвечает на корневой вопрос: почему компрометация *одного* ящика вообще способна уронить IP, с которого ходит вся легитимная почта? Потому что они делят один адрес. Разведите их.
Архитектура: submission → policy-оценка по sasl_username → развилка на разные транспорты с разными smtp_bind_address. Чистый пул для доверенного трафика, карантинный «жертвенный» пул для подозрительного-но-не-подтверждённого, hold — для заблокированного. Транзакционная почта и temp-mail трафик evilmail тоже идут по своим адресам, отдельным от пользовательской отправки.
Механика на Postfix. Карта отправитель → транспорт:
# main.cf
sender_dependent_default_transport_maps = hash:/etc/postfix/sender_transportТранспорты с привязкой к конкретному IP в master.cf:
outbound-clean unix - - n - - smtp
-o smtp_bind_address=212.22.69.211
outbound-quarantine unix - - n - - smtp
-o smtp_bind_address=212.22.69.212И — обязательно — на каждый исходящий IP свой полный набор аутентификации, иначе изоляция бессмысленна: приёмник всё равно свяжет письма через SPF/DKIM домена. Раздельные PTR (обратные записи у провайдера), SPF, ключи DKIM и политика DMARC:
; SPF домена перечисляет оба пула
evilmail.pro. TXT "v=spf1 ip4:212.22.69.211 ip4:212.22.69.212 -all"
; DMARC в reject с агрегатными отчётами
_dmarc.evilmail.pro. TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"Когда .212 попадёт в CSS из-за очередного слитого пароля — легитимная почта продолжает уходить с чистого .211, а вы спокойно делистите жертвенный адрес.
Восстановление после инцидента
Если IP всё-таки попал в списки, порядок такой.
- Spamhaus. Проверить, в каком именно списке — SBL/XBL/PBL или поведенческий CSS. Для CSS и XBL есть self-service removal через Blocklist Removal Center. Важно: не подавать заявку, пока не закрыли причину — повторное попадание удлиняет каждый следующий делистинг.
- Microsoft. Зарегистрировать IP в SNDS (snds.live.com), смотреть complaint rate и trap hits, при необходимости — форма mitigation. Outlook прощает медленно.
- Google. Postmaster Tools: spam rate, domain и IP reputation. Gmail реагирует на исправление за 2–4 недели ровного трафика.
- FBL. Подписаться на feedback loops (Yahoo/AOL CFL, Microsoft JMRP), чтобы жалобы приходили вам, а не копились молча. Для полноты картины проверить Sender Score и Cisco Talos.
После делистинга нельзя возвращать полный объём разом — это ретриггерит поведенческие фильтры. Ступенчатый прогрев: начать с малой доли обычного трафика на самый ценный домен получателя и наращивать по мере того, как reputation в Postmaster держится в зелёном. По срокам — от пары недель на Gmail до полутора-двух месяцев на Outlook/SNDS.
Чеклист внедрения
- Базовая линия по каждому ящику — знать нормальный messages/hour и типичное число уникальных получателей, иначе не с чем сравнивать всплеск.
- Soft/hard пороги по `sasl_username` — 100/час →
450 4.7.1, 300/час → блок. Именно по SASL, не по IP. - Авто-hold очереди триггером от policy-демона:
postqueue -j | jq | postsuper -h. - Kill-switch SASL —
enabled=falseв passdb плюсdoveadm kickдля активных сессий. - Отдельный outbound-пул + карантинный IP —
sender_dependent_default_transport_mapsиsmtp_bind_address, раздельные PTR/SPF/DKIM/DMARC. - Мониторинг SNDS, Google Postmaster и RBL — с алертом, а не ручным заходом раз в неделю.
Пароли будут утекать — фишинг, переиспользование, слитые базы. Это данность, а не то, что вы контролируете. Контролируете вы одно: сколько урона успеет нанести один скомпрометированный логин, прежде чем система его остановит. Правильно построенные лимиты, автоблок и изоляция пула превращают «сожгли IP, две недели делистинга» в «аккаунт заблокирован через 90 секунд, пользователь получил письмо о смене пароля, репутация не шелохнулась».


