[Часть 4]Три дня без policy-сервера: как iRedAPD умирает молча
Я собирался поставить rate limit на один почтовый адрес после мейл-бомбы. Для этого в iRedMail есть iRedAPD с плагином throttle. Полез настраивать и обнаружил, что policy-сервер не работает. Уже три дня.
История поучительна не столько поломками — они тривиальны, — сколько тем, как незаметно они прошли.
Первый признак
Смотрю конфиг Postfix и вижу закомментированное:
smtpd_recipient_restrictions =
reject_non_fqdn_recipient
reject_unlisted_recipient
# check_policy_service inet:127.0.0.1:7777
permit_mynetworks
...
При этом cron исправно гоняет /opt/iredapd/tools/cleanup_db.py — служебные скрипты iRedAPD на месте. То есть кто-то (я, при разборе другой проблемы) отключил обращение к политике, чтобы почта ходила, и забыл.
systemctl status iredapd
Active: failed (Result: exit-code) since Fri 2026-07-10 10:56:11 MSK; 3 days ago
ModuleNotFoundError: No module named 'web'
Три дня. Всё это время не работали:
- greylisting — основной отсекатель ботнетов;
- throttle — тот самый rate limit, за которым я и пришёл;
- reject_sender_login_mismatch — запрет отправлять от чужого имени;
- amavisd_wblist — белые и чёрные списки;
- sql_alias_access_policy — контроль доступа к алиасам.
Именно в это окно и прилетела мейл-бомба.
Поломка первая: web.py
File "/opt/iRedAPD-4.3/libs/utils.py", line 19, in <module>
from web import sqlquote
ModuleNotFoundError: No module named 'web'
Не хватает модуля web.py. Скорее всего слетел при обновлении пакетов или смене минорной версии Python.
apt install python3-webpy
# или: pip3 install web.py --break-system-packages
Демон стартует. Радоваться рано.
Поломка вторая: диалект SQLAlchemy
Сервис active (running), все плагины загружены:
iredapd Loading plugin (priority: 90): reject_sender_login_mismatch
iredapd Loading plugin (priority: 80): greylisting
iredapd Loading plugin (priority: 60): throttle
iredapd Loading plugin (priority: 50): sql_alias_access_policy
iredapd Loading plugin (priority: 40): amavisd_wblist
Порт слушается. Postfix к нему обращается. Письма ходят.
Но в /var/log/iredapd/iredapd.log:
Error while creating SQL connection:
NoSuchModuleError("Can't load plugin: sqlalchemy.dialects:postgres")
...
Unexpected error: AttributeError("'NoneType' object has no attribute 'connect'").
Fallback to default action: DUNNO
[198.51.100.25] RCPT, sender -> recipient, DUNNO [...]
<!> Error while logging smtp action:
AttributeError("'NoneType' object has no attribute 'execute'")
Каждый запрос падает на обращении к базе и возвращает DUNNO — «пропустить». То есть демон жив, зелёный в systemctl, отвечает Postfix — и при этом не выполняет ни одной проверки.
Причина: диалект postgres удалён в SQLAlchemy 1.4+. Теперь он называется postgresql. У меня стоит 1.4.46, а iRedAPD 4.3 передаёт старое имя.
Находим:
grep -rn "dbn" /opt/iRedAPD-4.3/libs/ | grep -v '^\s*#'
/opt/iRedAPD-4.3/libs/utils.py:257: dbn = 'postgres'
Правим:
cp /opt/iRedAPD-4.3/libs/utils.py /opt/iRedAPD-4.3/libs/utils.py.bak
sed -i "257s/dbn = 'postgres'/dbn = 'postgresql'/" /opt/iRedAPD-4.3/libs/utils.py
Обязательно снести кэш, иначе Python подхватит старый байткод:
rm -rf /opt/iRedAPD-4.3/libs/__pycache__
rm -f /opt/iRedAPD-4.3/libs/utils.pyc
systemctl restart iredapd
После рестарта строки NoSuchModuleError из лога исчезают.
Порядок включения важен
Соблазн велик: раскомментировать check_policy_service заодно с остальными правками конфига. Не надо.
Postfix при недоступном policy-сервере останавливает приём почты. Если демон не слушает 7777, вы получите не деградацию, а полный отказ.
Правильная последовательность:
# 1. убедиться, что демон жив
systemctl status iredapd
ss -lntp | grep 7777
# 2. убедиться, что он видит БД
tail -20 /var/log/iredapd/iredapd.log # без NoSuchModuleError
# 3. и только теперь включать
postconf -e 'smtpd_recipient_restrictions = ... check_policy_service inet:127.0.0.1:7777'
postconf -e 'smtpd_end_of_data_restrictions = check_policy_service inet:127.0.0.1:7777'
postfix check && systemctl reload postfix
# 4. смотреть лог во время первого письма
tail -f /var/log/maillog
Экстренное отключение, если что-то пошло не так:
postconf -e 'smtpd_end_of_data_restrictions ='
postconf -e 'smtpd_recipient_restrictions = reject_non_fqdn_recipient reject_unlisted_recipient permit_mynetworks permit_sasl_authenticated reject_unauth_destination'
systemctl reload postfix
Мелочь про SRS
При старте iRedAPD поднимает каналы SRS на портах 7778 и 7779. В конфиге iRedMail есть закомментированные строки:
#sender_canonical_maps = tcp:127.0.0.1:7778
#recipient_canonical_maps = tcp:127.0.0.1:7779
Не включайте их бездумно. У меня sender_canonical_maps уже занят под переписывание системного отправителя (hash:/etc/postfix/canonical) — подключение SRS сломало бы это. К тому же SRS нужен для пересылки чужой почты, а при отправке через транзакционный релей Return-Path и так подменяется на стороне сервиса.
Проверка, что всё заработало
Отправьте письмо с внешнего ящика и смотрите:
tail -f /var/log/iredapd/iredapd.log
Должны появиться осмысленные вердикты без Unexpected error перед ними и без Error while logging smtp action после.
Если отправитель не в белом списке — увидите 451 4.7.1 и задержку на несколько минут. Это greylisting, и это признак, что политика работает.
Не удивляйтесь, если greylisting не сработает на письме с Gmail или Яндекса: iRedAPD автоматически вносит крупные почтовики в белый список через spf_to_greylist_whitelists.py — тот самый скрипт, что крутится в cron.
Мина замедленного действия
Обе поломки — одного класса: iRedAPD 4.3 старше библиотек в дистрибутиве. Сначала пропал web.py, потом SQLAlchemy выкинул старое имя диалекта.
Впереди третья. В трейсбеке при первом падении мелькала строка:
import asyncore
asyncore удалён в Python 3.12. На Debian 12 (Python 3.11) он ещё есть, с предупреждением. На Debian 13 демон не запустится вовсе — а теперь Postfix от него зависит, и это означает остановку входящей почты сразу после апгрейда.
Так что заплатки, которые я поставил, — временные. В бэклог идёт обновление iRedAPD до актуальной версии, где всё это давно поправлено.
Главный вывод
Поломки тривиальные. Проблема в другом: три дня деградации никто не заметил.
Причём деградация была двухуровневая, и второй уровень коварнее первого. Когда демон падает — это хотя бы видно в systemctl status. Когда он запущен, зелёный, отвечает на запросы и при этом возвращает DUNNO на каждый — снаружи всё выглядит идеально.
Такие отказы не ловятся проверкой «процесс жив» и не ловятся проверкой «порт слушается». Нужна проверка результата работы.
Минимальный набор для policy-сервера:
| Проверка | Ловит |
|---|---|
systemctl is-failed iredapd | падение демона |
| TCP-порт 7777 | зависание без падения |
grep -c "Unexpected error" iredapd.log за последний час | потерю доступа к БД |
grep -c "Fallback to default action" iredapd.log | любую внутреннюю ошибку плагинов |
Последние две — как раз про «зелёный, но не работает».
И общее правило, которое я вынес из всего этого дня: если компонент существует ради проверок, мониторить надо не его наличие, а его срабатывания. Ноль срабатываний антиспама за сутки — это не «чистый трафик». Это, скорее всего, сломанный антиспам.