Почему mailcow за реверс-прокси не видит настоящий IP: бан не срабатывает и в логах не тот адрес
Если mailcow стоит за реверс-прокси не в его собственной docker-сети, он по умолчанию видит не клиента, а прокси: и в логах, и в бане Netfilter. Разбираю на кейсе, где именно теряется адрес — от TRUSTED_PROXIES в mailcow.conf до того, почему локальный бан всё равно не останавливает трафик из-за прокси, и как это лечится штатным экспортом решений Netfilter.
Симптом: SOGo банит один и тот же IP, а вход у всех сотрудников
Классическая картина, с которой ко мне обычно приходят: администратор ставит корпоративную почту на mailcow, выносит вход через отдельный реверс-прокси — Traefik, HAProxy или nginx на другом хосте — и через пару недель видит в логах SOGo один и тот же IP для всех пользователей. Это IP прокси. Если у прокси публичный адрес, Netfilter mailcow банит его же — по умолчанию после 10 неудачных попыток за 600 секунд (Fail2ban parameters: max_attempts и retry_window) — и блокирует доступ вообще всем, потому что для mailcow «атакующий» и «легитимный офис» — один и тот же адрес. Частные адреса (RFC 1918) Netfilter не банит никогда, поэтому с прокси в локальной сети этот вариант не проявляется.
Второй вариант того же симптома — зеркальный: mailcow честно определяет настоящий IP клиента, добавляет его в бан-лист Netfilter, а перебор паролей через веб-почту продолжается. Прокси как стоял между атакующим и mailcow, так и стоит: запись в списке блокировок появилась, но правило iptables, которое netfilter-mailcow ставит на хосте mailcow, ловит пакеты от адреса клиента, а к mailcow приходят пакеты от прокси. Оборвать соединение на своём уровне mailcow не может — фильтровать обязан прокси или firewall перед ним.
Отдельно замечу: сам по себе факт, что mailcow видит IP прокси вместо клиента, — не баг и не поломка. Это ожидаемое поведение любого веб-сервера за реверс-прокси, если явно не настроено доверие к заголовкам. Проблема не в том, что mailcow «не умеет» определять IP — а в том, что администратор ставит прокси и забывает договорить mailcow, кому и в каком объёме доверять.
Как mailcow определяет доверенный источник по умолчанию
Документация формулирует коротко: mailcow доверяет как прокси адресу docker-шлюза 172.22.1.1. В самом шаблоне nginx-mailcow (sites-default.conf.j2) доверие шире: set_real_ip_from прописан для 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 и fc00::/7, реальный адрес берётся из real_ip_header X-Forwarded-For с real_ip_recursive on. Поэтому прокси на том же хосте или в частной сети работает без дополнительных настроек, если он передаёт заголовки из примера документации: proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;.
Проблема начинается, когда прокси приходит к mailcow с публичного, маршрутизируемого адреса: edge-сервер на VPS, облачный балансировщик, CDN. Тогда внутренний nginx mailcow заголовок X-Forwarded-For игнорирует: источник не входит в список доверенных, и как адрес клиента фиксируется тот, с которого реально пришёл TCP-пакет, то есть адрес прокси.
Проверить топологию своей инсталляции легко: если docker compose exec nginx-mailcow env | grep TRUSTED показывает пустую переменную, а прокси подключается к mailcow с публичного адреса, значит защита от перебора паролей у вас прямо сейчас видит не того, кого нужно, и любой отчёт по безопасности, построенный на логах mailcow, будет показывать один и тот же «виновный» адрес прокси вместо реальной картины атак. Общий механизм realip, PROXY protocol и проверки заголовка Forwarded я разбирал отдельно — здесь беру только то, что специфично для mailcow.
TRUSTED_PROXIES: как научить mailcow доверять внешнему прокси
Решение для этого случая появилось в релизе mailcow 2025-02 (27.02.2025, коммит a567d5d) — переменная TRUSTED_PROXIES в mailcow.conf. Синтаксис из документации: TRUSTED_PROXIES=#.#.#.# — можно указать один адрес, диапазон CIDR или список через запятую, если прокси-серверов несколько. После правки mailcow.conf изменение не подхватывается на лету: нужно пересоздать контейнер nginx-mailcow командой docker compose up -d, чтобы при старте он перечитал переменную окружения и дописал адрес в set_real_ip_from сгенерированного /etc/nginx/includes/sites-default.conf.
Проверить, что заголовок реально подхватился, проще всего логами самого mailcow. Образ собран на nginx:alpine, где access.log направлен в stdout, поэтому смотрю через docker compose logs -f --tail=50 nginx-mailcow — если после правки TRUSTED_PROXIES и пересоздания контейнера в логе вместо адреса прокси появился адрес клиента, значит nginx-mailcow действительно доверяет источнику и берёт адрес посетителя из X-Forwarded-For, а не адрес балансировщика.
Если прокси-серверов несколько — например, отдельный внешний балансировщик и CDN перед ним — важно понимать, как nginx читает цепочку. С real_ip_recursive on он идёт по X-Forwarded-For справа налево и останавливается на первом адресе, которому не доверяет. Если в TRUSTED_PROXIES только балансировщик, клиентом окажется узел CDN. Значит, в список нужны все свои прыжки: адрес балансировщика и опубликованные диапазоны CDN (через запятую). Либо цепочку разбирает сам балансировщик и передаёт mailcow уже одно чистое значение.
TRUSTED_PROXIES не чинит бан сам по себе: нужен экспорт Netfilter наружу
Здесь я обычно ловлю вторую часть проблемы — ту, которую TRUSTED_PROXIES не решает. netfilter-mailcow работает в сетевом пространстве хоста и ставит правила iptables (или nftables) на самом хосте mailcow. Даже когда в логах уже виден правильный IP атакующего и он честно добавлен в бан-лист mailcow, сам трафик всё равно идёт через прокси на другом хосте, а значит правило iptables внутри mailcow ничего не блокирует на пути пакета — прокси как принимал соединение, так и продолжает его принимать и проксировать дальше.
Для этого случая в mailcow с релиза 2023-12 есть штатная функция «Provide Netfilter decisions via URL» (раздел Configuration & Details, флажок «Manage Fail2Ban externally»). После включения mailcow публикует URL со списком забаненных IP, который можно перегенерировать в интерфейсе при компрометации. Учтите два момента. Первый: в этом режиме netfilter-mailcow продолжает считать попытки и вести список, но локальные правила iptables больше не ставит — блокировка целиком переезжает на внешний узел. Второй: в релизе 2025-01 endpoint перенесли в отдельный PHP-файл, поэтому URL, скопированный до этого обновления, нужно заново взять в Admin UI. Дальше этот список нужно скормить тому узлу, который реально видит клиентский трафик до прокси — внешнему firewall или самому прокси-хосту. В документации mailcow как потребитель списка прямо приведён OPNsense, но идея работает с любым firewall, который умеет периодически подтягивать список по URL и применять его как источник блокировки.
Важно понимать порядок причин: TRUSTED_PROXIES решает задачу видимости — какой адрес попадёт в лог и в бан-лист. Экспорт Netfilter во внешний firewall решает задачу применения — где физически оборвётся соединение атакующего. Это две независимые настройки, и типичная ошибка — сделать только первую, порадоваться правильному IP в логах и решить, что защита восстановлена, хотя реального блокирования трафика как не было, так и нет.
Кейс «ХакГараж»: Traefik перед mailcow, бан не держал вход в SOGo
У клиента — хакерспейса «ХакГараж», 50 рабочих мест, где я по регламенту эксплуатации mailcow веду плановые проверки раз в квартал — mailcow стоял за Traefik на отдельном edge-хосте, который также терминировал TLS для десятка других сервисов проекта. После смены пароля у одного из аккаунтов кто-то начал перебор через веб-почту SOGo; Netfilter mailcow честно фиксировал попытки и банил IP — но вход оставался открытым ещё почти сутки, потому что бан существовал только внутри mailcow, а сам трафик Traefik продолжал принимать и проксировать без ограничений.
Чинили в два шага. Сначала добавили TRUSTED_PROXIES=<адрес Traefik> в mailcow.conf и пересоздали nginx-mailcow — в логах сразу появился настоящий IP атакующего вместо адреса Traefik, это дало точную картину атаки. Затем включили «Manage Fail2Ban externally», получили URL со списком банов и на самом edge-хосте настроили правило, которое раз в минуту подтягивает список и добавляет адреса в цепочку DROP перед Traefik. С этого момента бан из mailcow действительно останавливает трафик, а не просто ведёт статистику внутри контейнера, который атакующий даже не видит.
На разбор ушло около трёх часов, из них большая часть — на то, чтобы вообще понять, что дело не в самом Netfilter, а в топологии сети: администратор клиента был уверен, что раз бан появляется в интерфейсе, значит атака остановлена, и почти сутки смотрел не в ту сторону. После починки за две недели наблюдений список внешних банов на edge-хосте «ХакГаража» пополнялся четыре раза, и ни разу подбор пароля не продержался дольше одной минуты — ровно интервала опроса списка.
Частые ошибки при настройке TRUSTED_PROXIES
Первая ошибка — путают подсеть докер-сети mailcow с адресом внешнего прокси и вписывают в TRUSTED_PROXIES диапазон 172.22.1.0/24, хотя прокси стоит на совершенно другом хосте с белым IP. Толку ноль: этот диапазон и так входит в доверенный 172.16.0.0/12 из шаблона, а публичный адрес прокси по-прежнему не доверен. mailcow должен доверять именно тому адресу, с которого физически приходит TCP-соединение, а не внутренней докер-подсети, которая к внешнему прокси отношения не имеет. Проверяется это одной командой: docker compose exec nginx-mailcow grep set_real_ip_from /etc/nginx/includes/sites-default.conf — после четырёх штатных частных диапазонов там должна появиться строка с внешним адресом прокси.
Вторая — забывают, что правка mailcow.conf требует пересоздания контейнера, а не просто docker restart: docker compose up -d пересобирает конфигурацию nginx-mailcow с новой переменной окружения, тогда как обычный restart поднимает тот же контейнер со старыми настройками. Третья — путают адрес из HTTP-заголовка с фактическим сетевым источником: если TRUSTED_PROXIES не настроен, а X-Real-IP из прокси просто «поверили» на глаз по логам приложения за прокси, реальной защиты это не даёт — Netfilter mailcow банит то, что видит на уровне TCP-соединения, а не то, что написано в заголовке недоверенного источника.
Четвёртая ошибка встречается уже после того, как TRUSTED_PROXIES настроен правильно: администратор считает вопрос закрытым и не трогает сам прокси. Но если на прокси (Traefik, HAProxy, nginx) нет отдельного rate-limit или ограничения на число попыток входа в SOGo/Roundcube, прокси честно пропустит вперёд тысячу запросов в минуту ещё до того, как mailcow успеет их увидеть и посчитать. Похожая логика двойной защиты подробно разобрана в статье про fail2ban для Zimbra: TRUSTED_PROXIES чинит видимость адреса, а не пропускную способность — эти две задачи стоит решать вместе, а не вместо друг друга.
Как проверить, что защита действительно работает, а не только выглядит настроенной
После правки TRUSTED_PROXIES и включения экспорта Netfilter я всегда прогоняю простую проверку с тестового внешнего IP (не из офиса и не из докер-сети): несколько неудачных входов в SOGo подряд, затем сверка трёх мест — во-первых, docker compose logs nginx-mailcow --tail=50 должен показать именно тестовый внешний адрес, а не адрес прокси; во-вторых, в интерфейсе mailcow (Configuration → Configuration & Details → Configuration → Fail2ban parameters) этот адрес должен появиться в списке заблокированных; в-третьих, повторный запрос с того же адреса должен реально не проходить дальше edge-firewall — это проверяется прямым curl с внешней машины, а не из внутренней сети.
Если первый пункт не подтверждается — TRUSTED_PROXIES не подхватился, ищите ошибку в синтаксисе переменной или забытый docker compose up -d. Если не подтверждается второй — проблема не в определении IP, а в правилах самого Netfilter или в том, что защита от перебора вообще отключена для этого сервиса. Если не подтверждается третий при том, что первые два в порядке — значит список банов публикуется, но edge-firewall его не забирает или забирает слишком редко; я обычно ставлю интервал опроса в одну минуту, этого достаточно для веб-почты и не создаёт лишней нагрузки на API mailcow.
Частые вопросы
У меня mailcow и прокси в одном docker-compose файле — нужен ли TRUSTED_PROXIES?
Нет. Шаблон nginx-mailcow уже доверяет шлюзу 172.22.1.1 и всем частным диапазонам (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7). TRUSTED_PROXIES нужен, когда прокси приходит к mailcow с публичного адреса.
Я прописал TRUSTED_PROXIES, но в логах всё ещё IP прокси — что не так?
Чаще всего забыли `docker compose up -d` после правки mailcow.conf — переменная не подхватывается обычным restart. Проверьте также, что прокси действительно передаёт X-Forwarded-For: именно его читает nginx-mailcow, без него подставлять просто нечего.
Можно ли указать в TRUSTED_PROXIES несколько прокси или подсеть?
Да, документация допускает CIDR и список через запятую. Важно указывать реальные адреса, с которых mailcow видит входящие соединения, а не внутреннюю докер-подсеть.
Зачем включать «Manage Fail2Ban externally», если TRUSTED_PROXIES уже настроен?
TRUSTED_PROXIES чинит только видимость адреса в логах и веб-интерфейсе. Сам бан Netfilter по-прежнему применяется внутри mailcow и не останавливает трафик, который физически проходит через внешний прокси. Экспорт списка банов нужен, чтобы применить блокировку там, где реально идёт трафик.
Что использовать как потребителя списка банов, если у меня не OPNsense?
В документации OPNsense приведён просто как пример. Подходит любой firewall или сам прокси-хост, который может периодически забирать список по URL и добавлять адреса в правило блокировки — вплоть до простого cron-скрипта с iptables на edge-хосте.
Чем эта статья отличается от общей темы realip за nginx/HAProxy?
Общие механизмы realip, PROXY protocol и проверка заголовка Forwarded разобраны в отдельной статье про настоящий IP за прокси без доверия к поддельному XFF. Здесь — именно про специфику mailcow: переменную TRUSTED_PROXIES, доверенный шлюз докер-сети и штатный экспорт решений Netfilter во внешний firewall.
Источники
- mailcow docs — Reverse Proxy: Nginx — Пример конфигурации X-Real-IP/$remote_addr, X-Forwarded-For/$proxy_add_x_forwarded_for и требование TRUSTED_PROXIES в mailcow.conf при прокси в другой подсети. https://docs.mailcow.email/post_installation/reverse-proxy/r_p-nginx/
- mailcow docs — Reverse Proxy: Overview — Доверенный по умолчанию docker-шлюз 172.22.1.1, требования к HTTP_BIND/HTTPS_BIND и ADDITIONAL_SERVER_NAMES. https://docs.mailcow.email/post_installation/reverse-proxy/r_p/
- mailcow docs — Netfilter: Provide Netfilter decisions via URL — Функция «Manage Fail2Ban externally», публикация и перегенерация URL со списком банов, пример потребителя — OPNsense. https://docs.mailcow.email/manual-guides/mailcow-UI/u_e-mailcow_ui-netfilter/
- mailcow.email — 2025-02 Release notes — Запись «[Nginx] Add support for trusted proxies via env var», дата 27.02.2025, коммит a567d5d. https://mailcow.email/posts/2025/release-2025-02/
- mailcow community — IP from my proxy coming over to nginx container — Обсуждение поведения TRUSTED_PROXIES на практике и подтверждение, что переменная добавляет адрес в set_real_ip_from. https://community.mailcow.email/d/4749-ip-from-my-proxy-coming-over-to-nginx-container



