> MARAT_SHVETS.portfolio
> ← все новости

Поднимаем свою почту: Stalwart, Bulwark и вечная борьба за порт 443

Развернули полностью self-hosted почтовый стек — от SMTP-сервера до веб-клиента — и по пути наступили почти на все возможные грабли: конфликт портов, CSP-блокировки, спам-фильтры и автонастройку на iPhone. Рассказываем, что пошло не так и как это чинили.

Зачем

Решили отказаться от сторонних почтовых сервисов и поднять полностью свою инфраструктуру — почтовый сервер и веб-клиент, целиком под своим контролем, без зависимости от чужих SaaS. В качестве стека выбрали Stalwart (современный почтовый сервер на Rust с поддержкой JMAP, IMAP, POP3, SMTP) и Bulwark — веб-клиент нового поколения, построенный поверх JMAP.

Всё — в Docker, за реверс-прокси nginx-proxy-manager. По пути пришлось решить целый набор проблем, каждая из которых по отдельности звучит просто, а вместе — неплохой чек-лист для всех, кто будет поднимать что-то похожее.

Проблема №1: два сервиса, один порт 443

Первая же неожиданность — Stalwart из коробки публикует наружу порты 80 и 443 для своей веб-админки. NPM тоже претендует на эти порты для терминации TLS у всех проксируемых сервисов. Конфликт неизбежен.

Решение — убрать прямую публикацию 80/443 у Stalwart, оставить эти порты только за NPM, а Stalwart и Bulwark посадить в общую docker-сеть, чтобы прокси достучался до них по имени контейнера, без единого лишнего порта на хосте:

docker network create proxy-net

Наружу у почтового сервера остаются только протокольные порты — 25, 465, 587, 993, 995, 143, 110, 4190 — которые реверс-прокси всё равно не умеет проксировать (он работает только с HTTP/HTTPS).

Проблема №2: сломанное автопродление TLS-сертификатов

Пока Stalwart сам держал порт 443, он и сам продлевал себе сертификат через ACME (tls-alpn-01) — способ, который требует прямого доступа снаружи именно к 443-му порту сервера. Отобрав порт в пользу прокси, мы молча сломали автопродление.

Переключили ACME-провайдера Stalwart на dns-01 — теперь продление сертификата идёт через DNS API, вообще не завязано на то, кто держит 443.

Проблема №3: OAuth и WebAdmin ломались за прокси

После переезда за NPM веб-админка Stalwart начала странно себя вести при логине — URL для OAuth/JMAP discovery строились с внутренним docker-хостнеймом вместо публичного домена. Решение нашлось в официальной документации Stalwart: переменная окружения STALWART_PUBLIC_URL, которая явно указывает серверу, каким адресом его видит внешний мир.

Проблема №4: Content Security Policy блокирует логин в Bulwark

Самая неочевидная — Bulwark отказывался логинить пользователей с ошибкой "Unable to reach the server", при этом логи контейнера молчали. Разгадка нашлась в консоли браузера: строгая CSP-политика Bulwark (connect-src 'self' https:) запрещает фронтенду делать запросы на что угодно, кроме HTTPS-адресов. А в конфиге JMAP-сервера был прописан внутренний http://stalwart:8080 — браузер такие запросы просто не пропускал, ещё до попытки соединения.

Поменяли на публичный адрес, который уже проксируется через NPM — заработало сразу.

Проблема №5: спам-фильтры и репутация

Прогнали письма через mail-tester.com — получили не идеальный, но рабочий счёт. Разобрали каждый пункт:

DKIM/SPF — прошли сразу, отлично
RDNS_NONE — не хватало PTR-записи для исходящего IP, донастроили у хостинг-провайдера
FROM_FMBLA_NEWDOM — штраф за свежезарегистрированный домен, тут ничего не поделать, кроме как подождать пару недель

Проблема №6: автонастройка на устройствах

Чтобы пользователи могли просто ввести email и пароль, а не вбивать вручную IMAP/SMTP-серверы, настроили сразу три протокола автообнаружения:

Autodiscover (Microsoft, для Outlook) — через поддомен autodiscover.
Autoconfig (Mozilla, для Thunderbird) — через поддомен autoconfig.
Для iPhone это работает менее предсказуемо, поэтому сделали .mobileconfig-профиль — пользователь открывает файл, iOS сама настраивает учётку целиком, пароль запрашивается отдельно и нигде не хранится

Проблема №7: Stalwart заблокировал собственный реверс-прокси

Спустя какое-то время после переезда веб-админки за NPM она внезапно перестала отвечать — прокси не мог достучаться до бэкенда, хотя ещё недавно всё работало исправно. В логах Stalwart нашлась говорящая строка:

INFO Blocked IP address (security.ip-blocked) listenerId = "http", localPort = 8080, remoteIp = 172.18.0.4

172.18.0.4 — внутренний docker-адрес самого контейнера NPM. Встроенная в Stalwart защита от брутфорса восприняла регулярные запросы прокси как подозрительную активность и заблокировала его IP.

Решили через официальный stalwart-cli (начиная с версии 0.16 он больше не идёт внутри Docker-образа — ставится отдельно на хост). Сняли текущую блокировку и, что важнее, добавили IP прокси в белый список, чтобы блокировка не повторялась:

stalwart-cli --url http://<адрес-сервера>:8080 --user admin --password <пароль> create AllowedIp --field address=172.18.0.4 --field reason="Nginx Proxy Manager"

Важный нюанс: добавление в белый список само по себе не снимает уже существующую блокировку — это защита на будущее, а не "ключ" от текущей. Нужны оба шага сразу.

Итог

Полностью self-hosted почтовый стек — от SMTP до веб-интерфейса — теперь работает предсказуемо, поддерживает автонастройку на всех основных клиентах и проходит спам-фильтры без проблем. Каждая из описанных проблем в отдельности решается за пару минут, если знать, куда смотреть — надеемся, этот разбор сэкономит кому-то вечер отладки.

← все новости→ написать мне