evgen@spb: ~/2026/07/chast-4-tri-dnya-bez-policy-servera-kak-ired-admin-umiraet-molcha/

[Часть 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любую внутреннюю ошибку плагинов

Последние две — как раз про «зелёный, но не работает».

И общее правило, которое я вынес из всего этого дня: если компонент существует ради проверок, мониторить надо не его наличие, а его срабатывания. Ноль срабатываний антиспама за сутки — это не «чистый трафик». Это, скорее всего, сломанный антиспам.

evgen@spb: ~/contacts
evgen@spbping -c1 evgen.info
64 bytes from evgen.info: отвечу в течение рабочего дня
evgen@spbcat contacts.txt
mail      ya@evgen.info
telegram  @EvgenOne
github    github.com/onegin
blog      evgen.info/blog
evgen@spbecho $ЗАЧЕМ_ПИСАТЬ
интересная инфраструктурная задача · вопрос по Proxmox или Icinga ·
желание обсудить, почему memtest молчал, а память была битая
evgen@spb
© 2026 Евгений Подолинский · Санкт-Петербург
синий здесь – не тот, о котором вы подумали.