Два автоответчика зациклились и сгенерировали 40 тысяч писем за ночь
Ночью очередь Postfix на почтовом сервере разрослась до нескольких тысяч писем, диск /var/spool/postfix начал заполняться, а утром в логах обнаружились без малого 40 000 исходящих сообщений между двумя внутренними адресами. Ни одного реального клиента снаружи, ни одной попытки авторизации — только два автоответчика, которые всю ночь отвечали друг другу. Разбираем, как выглядела эта петля в логах, какие версии проверили и отбросили, и почему штатная защита Postfix от зацикливания здесь не сработала.
Содержание
Что случилось: ночь, за которую сервер разослал 40 000 писем
Первым сработал мониторинг диска — заполнение /var/spool/postfix перевалило за 80%, дальше алерт на размер очереди. Дежурный инженер зашёл по SSH, выполнил postqueue -p | wc -l и увидел цифру, которая заставила проснуться окончательно: очередь на отправку насчитывала больше 6000 писем, а mailq -j показывал, что новые сообщения продолжают появляться каждые несколько секунд.
Первое, что бросилось в глаза — все письма шли между двумя одними и теми же адресами: support@ и tickets@ в одном и том же домене. Тема письма росла на каждом шаге: Заявка №41822 создана, Re: Заявка №41822 создана, Автоответ: Re: Заявка №41822 создана и так далее. Заголовки Message-ID у всех писем были разные, In-Reply-To честно указывал на предыдущее письмо — переписка была настоящей, просто вели её не люди.
К утру счётчик в логах mail.log за ночь показал почти 40 000 отправленных сообщений между этими двумя ящиками. Внешне для клиентов ничего не сломалось — доставка настоящей почты продолжала работать, просто с задержкой, потому что очередь была забита мусором. Но репутационный риск был реальным: часть петли всё же почти вырвалась наружу — один из циклов случайно зацепил внешний адрес в копии, и лишь то, что оба автоответчика использовали корректные SPF/DKIM-подписи основного домена, не позволило почтовым провайдерам сходу отправить это в спам-фильтр как аномалию.
Первая гипотеза: спам-атака или взломанный ящик
Первая мысль дежурного — либо взломали учётку и рассылают спам через SMTP AUTH, либо сервер стал открытым релеем и кто-то извне гоняет через него мусор. Обе версии проверялись быстро:
grep sasl_username /var/log/mail.logза последние 12 часов — ни одной посторонней авторизации, только служебные системные учётки, которые и должны были логиниться.- Проверка на открытый релей (
nmap --script smtp-open-relayс внешнего хоста) — сервер отказывал в пересылке для чужих доменов, релей закрыт. - IP-адреса подключений в
mail.log— все входящие SMTP-сессии, порождавшие эти письма, были локальными (127.0.0.1 или адрес самого сервера), внешних IP в цепочке не было вообще.
Версия с атакой отпала в первые десять минут: это было целиком внутреннее взаимодействие, без единого внешнего подключения.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверВторая гипотеза: зациклилась рассылка или крон-задача
Следующая версия — сломался Mailtrain или другой инструмент рассылок и повторно шлёт одну и ту же кампанию. Проверили логи Mailtrain — активных кампаний в эту ночь не было вообще, сервис даже не поднимался в списке процессов с новыми задачами.
Третья версия — задвоился cron. На сервере крутился самописный PHP-скрипт, который раз в минуту опрашивал почтовый ящик tickets@ по IMAP и создавал тикет с автоответом клиенту. Проверили crontab -l и /var/log/syslog по паттерну cron — задача выполнялась строго по расписанию, без дублирования запусков, лишних процессов php-cli в ps aux не висело. Значит, само зацикливание рождалось не из-за сбоя планировщика, а из-за содержимого писем, которые скрипт обрабатывал.
Только после того, как отбросили атаку, взлом и дублирование крона, стало ясно: нужно смотреть не на то, кто отправляет, а на то, что именно гоняется туда-обратно между двумя ящиками — и почему это не останавливается само.
Что показали логи Postfix и IMAP
Собрали полную цепочку по Message-ID и In-Reply-To из mail.log и из самих писем в очереди (postcat -q <id>). Картина сложилась такая:
- Реальный клиент написал на
support@с обычным вопросом. - На ящике
support@был активен Sieve-автоответчикvacation— сотрудник, отвечающий за поддержку, уходил в отпуск и включил "автоответ об отсутствии", который согласно давнему временному правилу форвардинга дублировал копию каждого входящего письма наtickets@для логирования (правило добавили месяцы назад при тестовой миграции на новую тикет-систему и забыли убрать). - Копия автоответа
vacationпопала вtickets@, где крутился самописный скрипт: он воспринял входящее письмо как новую заявку и по IMAP-поллингу тут же отправил на адрес отправителя (то есть наsupport@) письмо "Заявка №N создана, ожидайте ответа". - Это письмо снова попало на
support@, снова через то же правило форвардинга продублировалось наtickets@, снова было воспринято как новая заявка — и цикл пошёл по кругу без участия человека.
Ключевая деталь была в заголовках: ни Sieve-автоответчик, ни самописный скрипт не выставляли и не проверяли Auto-Submitted: auto-replied. По RFC 3834 именно этот заголовок должен стоять у автоматических ответов, и именно по нему второй автоответчик обязан понимать: "это не человек написал, отвечать не нужно". У обоих систем эта проверка попросту отсутствовала — каждая честно считала входящее письмо новым обращением человека.
# фрагмент из postcat -q для одного из писем цепочки
Message-ID: <b7f2e1a9-4d3c@mail.example>
In-Reply-To: <a1029384-cc11@mail.example>
Subject: Автоответ: Re: Заявка №41822 создана
Auto-Submitted: no
Заголовок Auto-Submitted: no — это не ошибка чтения лога, это буквально то, что стояло в письме: система-автор письма не считала его автоматическим, хотя по сути это была третья итерация одного и того же цикла.
Почему Postfix сам не остановил петлю
Логичный вопрос: у Postfix же есть защита от зацикливания писем — параметр hopcount_limit (по умолчанию 50), который должен обрывать сообщение, если оно проходит слишком много пересылок. Почему он не сработал здесь?
Ответ в том, как считается "хоп". hopcount_limit считает число заголовков Received: у одного и того же письма при его пересылке через несколько серверов — то есть защищает от петель именно форвардинга одного сообщения. В нашем случае каждый автоответчик не пересылал старое письмо, а генерировал новое — с новым Message-ID, новым конвертом и одним свежим Received:. Цепочка Received:-заголовков не накапливалась никогда, потому что каждое звено цепи было отдельным письмом с чистой историей. Для Postfix это выглядело как тысячи независимых нормальных писем, а не как одно письмо, зациклившееся само на себя.
Это важный нюанс для любого, кто рассчитывает на hopcount_limit как на универсальную защиту: он спасает от петель пересылки (.forward, алиасов, дублирования через несколько MTA), но не от петли двух самостоятельных генераторов автоответов. Здесь нужна отдельная логика.
Как остановили петлю за 20 минут
Пока разбирались с причиной, петля продолжала генерировать письма, так что сначала остановили процесс, а разбор доводили параллельно:
- Отключили правило форвардинга
support@ → tickets@в/etc/postfix/virtual, выполнилиpostmap virtualиpostfix reload— разорвали физический канал, по которому копии ходили между ящиками. - Деактивировали Sieve-автоответчик на
support@:doveadm sieve deactivate -u support@example.com vacation.sieve. - Остановили cron-задачу самописного скрипта:
crontab -eу сервисного пользователя, закомментировали строку опросаtickets@, дождались завершения текущего запуска. - Почистили очередь от мусора. Здесь стоит быть аккуратным:
postsuper -d ALLудаляет вообще всё, включая настоящую почту, которая могла ждать отправки. Мы сначала отфильтровали конкретно письма между двумя проблемными адресами:
mailq | grep -B1 "tickets@example.com\|support@example.com" \
| awk '/^[A-F0-9]/{print $1}' | tr -d '*!' > /tmp/loop_ids.txt
postsuper -d $(cat /tmp/loop_ids.txt)
- Проверили
postqueue -pещё раз — убедились, что осталась только легитимная почта, дали ей уйти нормально.
От первого алерта до полной остановки цикла прошло около 20 минут, но за ночь до этого сервер успел сгенерировать почти 40 000 писем — большую часть времени никто не смотрел на мониторинг, потому что алерт на размер очереди был настроен с порогом, который сработал слишком поздно.
Что изменили, чтобы это не повторилось
Разбор инцидента закончился не восстановлением работоспособности, а списком изменений, которые убирают саму возможность повтора:
- Проверка
Auto-Submittedв обе стороны. Sievevacationпо спецификации сам добавляетAuto-Submitted: auto-repliedв свои письма и по умолчанию не отвечает на письма с таким же заголовком у входящих — эта защита была, но только с одной стороны. В самописный скрипт добавили явную проверку: если у входящего письма есть заголовокAuto-Submittedсо значением, отличным отno, заявка не создаётся и автоответ не отправляется. Заодно скрипт стал сам проставлять этот заголовок в исходящих автоответах. - Убрали временное двустороннее форвардирование. Правило
support@ → tickets@, оставшееся с незавершённой миграции на новую тикет-систему, удалили полностью. Если два почтовых обработчика должны временно работать параллельно на переходный период, для этого нужен один источник входящей почты и явное разделение зон ответственности, а не копирование писем в обе стороны "на всякий случай". - Дедупликация по отправителю на уровне скрипта. Добавили простую таблицу в SQLite: перед отправкой автоответа скрипт проверяет, не отвечал ли он этому же адресу за последние 30 минут, и если да — пропускает отправку и просто логирует событие. Это не идеальное решение (можно случайно не ответить на два разных обращения подряд от одного клиента), но оно останавливает экспоненциальный рост писем на раннем этапе, а не через несколько тысяч итераций.
- Rate limiting на уровне Postfix. Через
anvil-ограничения добавилиsmtpd_client_message_rate_limitдля локальной отправки с сервисных адресов — если один и тот же локальный отправитель превышает разумное число писем в минуту, дальнейшая отправка временно блокируется с логированием, вместо того чтобы уходить в очередь без ограничений. - Понизили порог алерта на размер очереди. Раньше алерт срабатывал при превышении нескольких тысяч писем в очереди — по факту это было уже глубоко в инциденте. Порог снизили до нескольких десятков писем с одновременной проверкой, не растёт ли очередь за последние 5 минут, а не только абсолютное значение.
- Ревизия всех правил форвардинга и автоответов раз в квартал. Отдельным пунктом в регламент добавили аудит
/etc/postfix/virtual,/etc/aliasesи активных Sieve-скриптов — искать временные правила, у которых давно прошёл срок действия, но которые никто не убрал.
Если у вас на сервере крутится своя почта, эти же грабли легко словить на связке двух любых систем с автоответами — не только тикет-системы и Sieve, но и CRM с уведомлениями, форм-мейлеров и ботов поддержки. Стоит один раз пройтись по всем точкам, где сервер генерирует письма без участия человека, и проверить, что каждая из них умеет распознавать чужой автоответ и не отвечает на него.
Разбор ошибок с очередью Postfix и с самой отправкой почты с сервера дополняют этот случай: переполнение очереди Postfix разбирает похожие симптомы с другой причиной, а когда почта не отправляется с сервера — обратную ситуацию, когда писем, наоборот, не хватает. Если разбираетесь с SPF/DKIM/DMARC на своём почтовом сервере, отдельная статья про частые ошибки настройки закрывает смежный класс проблем с доставляемостью. А чтобы подобные инциденты не решались через "кто-то заметил заполненный диск ночью", полезно свериться с антипаттерном мониторинга, который никто не смотрит.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро отличить петлю автоответчиков от реальной спам-атаки?
Смотрите на источник подключений в mail.log: если все SMTP-сессии, порождающие письма, идут с локального адреса сервера или между известными внутренними ящиками — это внутренняя петля, а не внешняя атака. Внешняя атака почти всегда даёт видимые чужие IP в логах подключений или следы взлома в SASL-авторизации.
Спасает ли hopcount_limit в Postfix от таких петель?
Только частично. Он защищает от зацикливания пересылки одного и того же письма (когда растёт число заголовков Received:), но не помогает, если каждая сторона генерирует новое письмо с чистым конвертом — именно так вели себя оба автоответчика в этом разборе.
Что делать прямо сейчас, если очередь Postfix уже забита тысячами писем от петли?
Сначала разорвите канал, по которому письма ходят между системами (форвардинг, алиас, автоответчик), потом удаляйте из очереди только письма между проблемными адресами точечно, а не всю очередь разом — postsuper -d ALL заодно удалит и настоящую почту, которая ждёт отправки.
Как настроить свой автоответчик, чтобы он не отвечал на чужие автоответы?
Проверяйте заголовок Auto-Submitted у входящего письма перед отправкой ответа — если он присутствует со значением, отличным от no, письмо считается автоматическим и ответ не отправляется. Дополнительно сам автоответчик должен проставлять этот заголовок в своих исходящих письмах, чтобы другие системы могли так же его распознать.
Нужен ли rate limiting для внутренней почты, если весь трафик и так локальный?
Да — именно локальный трафик между сервисными адресами чаще всего и порождает такие петли, потому что для него обычно не настроены ограничения, которые есть для внешних отправителей. Ограничение через anvil (smtpd_client_message_rate_limit) или простая дедупликация на уровне приложения останавливают рост писем на первых итерациях, а не после нескольких тысяч.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →