MAATRIX / Блог / Два автоответчика зациклились и сгенерировали 40 тысяч писем за ночь

Два автоответчика зациклились и сгенерировали 40 тысяч писем за ночь

MAATRIX

Ночью очередь 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>). Картина сложилась такая:

  1. Реальный клиент написал на support@ с обычным вопросом.
  2. На ящике support@ был активен Sieve-автоответчик vacation — сотрудник, отвечающий за поддержку, уходил в отпуск и включил "автоответ об отсутствии", который согласно давнему временному правилу форвардинга дублировал копию каждого входящего письма на tickets@ для логирования (правило добавили месяцы назад при тестовой миграции на новую тикет-систему и забыли убрать).
  3. Копия автоответа vacation попала в tickets@, где крутился самописный скрипт: он воспринял входящее письмо как новую заявку и по IMAP-поллингу тут же отправил на адрес отправителя (то есть на support@) письмо "Заявка №N создана, ожидайте ответа".
  4. Это письмо снова попало на 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 минут

Пока разбирались с причиной, петля продолжала генерировать письма, так что сначала остановили процесс, а разбор доводили параллельно:

  1. Отключили правило форвардинга support@ → tickets@ в /etc/postfix/virtual, выполнили postmap virtual и postfix reload — разорвали физический канал, по которому копии ходили между ящиками.
  2. Деактивировали Sieve-автоответчик на support@: doveadm sieve deactivate -u support@example.com vacation.sieve.
  3. Остановили cron-задачу самописного скрипта: crontab -e у сервисного пользователя, закомментировали строку опроса tickets@, дождались завершения текущего запуска.
  4. Почистили очередь от мусора. Здесь стоит быть аккуратным: 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)
  1. Проверили postqueue -p ещё раз — убедились, что осталась только легитимная почта, дали ей уйти нормально.

От первого алерта до полной остановки цикла прошло около 20 минут, но за ночь до этого сервер успел сгенерировать почти 40 000 писем — большую часть времени никто не смотрел на мониторинг, потому что алерт на размер очереди был настроен с порогом, который сработал слишком поздно.

Что изменили, чтобы это не повторилось

Разбор инцидента закончился не восстановлением работоспособности, а списком изменений, которые убирают саму возможность повтора:

  • Проверка Auto-Submitted в обе стороны. Sieve vacation по спецификации сам добавляет 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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →