870012514a
Document the preferred implementation order in the roadmap and linked plans. Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# План: inbound-relay (входящий релей)
|
|
|
|
**Статус:** согласовано
|
|
**Версия:** целевой bump **1.x** MINOR; **возможен 2.x** — требует уточнения по
|
|
итогам реализации (не фиксировать major заранее).
|
|
**Модель:** Opus (инфра/безопасность, риск open relay).
|
|
**Порядок:** рекомендуется после [web-split](web-split.md) и
|
|
[domain-admin](domain-admin.md).
|
|
|
|
---
|
|
|
|
## Цель
|
|
|
|
Возможность принимать почту на порт 25 для явно настроенных доменов и пересылать
|
|
её на заданный вышестоящий backend (роль backup-MX / relay-forwarder), **как
|
|
выключаемый по умолчанию модуль**, не затрагивающий поведение и поверхность
|
|
атаки базового исходящего релея.
|
|
|
|
## Зачем это нужно (сценарии)
|
|
|
|
- **Backup-MX** — принять почту, когда основной почтовый сервер домена временно
|
|
недоступен, и передать её, когда он вернётся.
|
|
- **Фронт для сервера без внешнего IP** — у оператора есть свой почтовый сервер,
|
|
который по каким-то причинам **сам не может принимать почту из интернета**
|
|
(нет статического/внешнего IP, за NAT, серый адрес, закрытый порт 25 на
|
|
входящую и т.п.). SelfPost с публичным IP и корректным PTR выступает
|
|
публичным входным узлом для домена (MX указывает на него) и пересылает почту
|
|
на этот внутренний/недоступный извне сервер.
|
|
|
|
## Граница объёма (критично — что это НЕ)
|
|
|
|
- **ЭТО:** приём на 25 для доменов из явного списка + пересылка (relay/forward)
|
|
на upstream (`relay_domains` + `transport_maps` + `relay_recipient_maps`).
|
|
Postfix здесь — чистый пересыльщик, без локальной доставки.
|
|
- **ЭТО НЕ (out of scope, [product.md](../product.md)):** локальная доставка в
|
|
почтовые ящики, IMAP/POP3, webmail, Dovecot. Никаких mailbox'ов. SelfPost
|
|
также **не реализует и не тянет в свой образ** движок антиспама/антивируса
|
|
(rspamd/ClamAV) — но, в отличие от прежней формулировки, и **не**
|
|
перекладывает фильтрацию на backend (см. блок «Антиспам» ниже): предоставляет
|
|
точку подключения внешнего фильтра.
|
|
|
|
## Почему как опция/плагин
|
|
|
|
- Приём на порт 25 меняет модель угроз (open relay для входящей, backscatter,
|
|
spam-ingress). Поэтому по умолчанию **выключено** флагом env
|
|
`INBOUND_RELAY_ENABLE=false`; включение — осознанный шаг оператора.
|
|
- Изоляция: отдельные таблицы SQLite, отдельные хендлеры/страницы панели,
|
|
отдельная ветка генерации конфига. При выключенном флаге входной listener,
|
|
таблицы и UI отсутствуют — базовый исходящий тракт байт-в-байт неизменен.
|
|
|
|
## Что делать
|
|
|
|
- Env-флаг `INBOUND_RELAY_ENABLE` (default false); при `true` — генерировать
|
|
входной сервис и его конфиг из состояния панели тем же путём, что остальной
|
|
конфиг (`postfix-config.sh`).
|
|
- **`master.cf`:** входной `smtp inet` на 25 для приёма из интернета (сейчас 25
|
|
используется только на исходящую доставку). Отдельный от 465/587: на 25 **не**
|
|
предлагается SASL и **не** разрешается отправка наружу — только приём для
|
|
`relay_domains`.
|
|
- **Анти-open-relay для входящей (обязательно):**
|
|
`smtpd_relay_restrictions`/`smtpd_recipient_restrictions` входного smtpd
|
|
принимают почту **только** для доменов из `relay_domains` и **только** для
|
|
известных получателей (`relay_recipient_maps`); всё прочее —
|
|
`reject_unauth_destination`/`reject_unlisted_recipient`. Открытый релей и приём
|
|
«для кого угодно» невозможны.
|
|
- **Backscatter:** предпочтительно знать валидных получателей (reject unknown
|
|
recipient на этапе RCPT), чтобы не порождать bounce на несуществующие адреса.
|
|
- **Панель управляет:** список входящих доменов; для каждого — upstream
|
|
destination (`host:port`, транспорт), опциональный список валидных
|
|
получателей, опциональный TLS к upstream. Строгая валидация домена/хоста/порта
|
|
(whitelist), injection-safe запись map-файлов (как `sender_login_maps` в Фазе
|
|
4), `os/exec` без shell ([security.md](../security.md)).
|
|
- **Милтеры:** OpenDKIM на входящем тракте не нужен (чужую входящую не
|
|
подписываем). journal-milter опционально переиспользовать для журнала входящих
|
|
(доп. работа) либо на первом этапе оставить входящий без него; поведение
|
|
fail-open сохраняется.
|
|
- **Rate-limit/размер:** грубый лимит по client IP (`anvil`, как L1) и
|
|
`message_size_limit` на входном smtpd.
|
|
|
|
## Антиспам (важная, но опциональная возможность)
|
|
|
|
Это ценная опция, но она **не обязательна**: часть операторов вполне устроит
|
|
**слепая пересылка без фильтрации** — например, когда backend сам умеет
|
|
фильтровать по содержимому, стоит доверенный upstream, или объём/риск невелик.
|
|
Поэтому антиспам-хук по умолчанию **выключен** (пустой
|
|
`INBOUND_ANTISPAM_MILTER`), и входящий релей полностью работоспособен без него.
|
|
|
|
Важно другое — где фильтрация возможна технически: при «слепом» relay целевой
|
|
backend видит подключающимся IP адрес **SelfPost**, а не исходного отправителя,
|
|
поэтому на backend'е ломается всё, что завязано на origin IP (DNSBL/репутация
|
|
проверяются против IP SelfPost, SPF даёт fail — SelfPost не входит в SPF
|
|
домена-отправителя). **Единственная точка, где ещё виден настоящий client IP —
|
|
входной хоп на SelfPost**; поэтому тем, кому фильтрация нужна, она должна быть
|
|
*подключаема именно здесь*, а не переложена на backend, который эту информацию
|
|
уже потерял.
|
|
|
|
Дизайн подключения:
|
|
|
|
- **Движок антиспама — отдельный опциональный контейнер** (rspamd и т.п.),
|
|
который оператор запускает **только если нужна эта опция** (тот же принцип,
|
|
что reverse-proxy — отдельный контейнер вне образа SelfPost). SelfPost его
|
|
**не содержит и не запускает** — образ и принцип «один контейнер, три
|
|
процесса» неизменны, [product.md](../product.md) out of scope не нарушается
|
|
(SelfPost не реализует антиспам).
|
|
- **SelfPost предоставляет точку подключения:** milter-хук на входном smtpd.
|
|
Адрес движка задаётся env (например,
|
|
`INBOUND_ANTISPAM_MILTER=inet:antispam:11332`, пусто → хук выключен) и
|
|
добавляется в `smtpd_milters` **только входного** тракта (не на 465/587).
|
|
Postfix передаёт milter'у настоящий client IP/HELO/PTR — фильтр видит
|
|
истинный origin. `milter_default_action` для этого milter'а — конфигурируемый
|
|
(fail-open vs tempfail); дефолт определить при реализации.
|
|
- **Нативный backstop без зависимостей:** на том же входном хопе доступны
|
|
средства Postfix по origin IP — `reject_rbl_client` (DNSBL), проверки
|
|
HELO/PTR — работают даже без внешнего контейнера. Плюс сохранение
|
|
аутентификации для downstream через ARC/`Received` там, где часть фильтрации
|
|
всё же остаётся на backend.
|
|
- **docker-compose:** задокументировать опциональный фрагмент antispam-сайдкара
|
|
(как альтернативные фрагменты reverse-proxy) — контейнер поднимается вместе со
|
|
стеком только при включённой опции.
|
|
- **Персистентность:** новые таблицы и map-файлы под `/data` — попадают в полный
|
|
бэкап автоматически (Фаза 9). Экспорт/импорт домена можно расширить входящей
|
|
конфигурацией — опционально, пометить.
|
|
- **DNS-документация:** для входящего домена нужна `MX`-запись, указывающая на
|
|
сервер (в отличие от исходящего, где MX не требуется) — отразить в разделе DNS
|
|
README.
|
|
|
|
## Безопасность
|
|
|
|
[security.md](../security.md): валидация ввода на сервере, экранирование записи
|
|
в конфиги, `exec` без интерполяции, никакого open relay, защита от backscatter.
|
|
|
|
## Готово, когда
|
|
|
|
При `INBOUND_RELAY_ENABLE=true` и настроенном домене письмо на порт 25 для этого
|
|
домена пересылается на заданный upstream; почта для ненастроенных
|
|
доменов/получателей отклоняется (не open relay, не backscatter); при заданном
|
|
`INBOUND_ANTISPAM_MILTER` входящая проходит через внешний фильтр с настоящим
|
|
origin IP (проверено сайдкар-контейнером), при пустом — хук не мешает; при
|
|
`INBOUND_RELAY_ENABLE=false` — входной порт/таблицы/UI отсутствуют, базовый
|
|
исходящий релей неизменён; `build`/`vet`/`test`/образ зелёные.
|
|
|
|
## Риски
|
|
|
|
- open relay/backscatter — снимается `relay_domains` + `relay_recipient_maps` +
|
|
`reject_unauth_destination`;
|
|
- потеря origin IP для фильтрации на backend'е при пересылке — снимается
|
|
milter-хуком антиспама + нативным DNSBL на входном хопе, где origin IP ещё
|
|
виден;
|
|
- порт 25 на приём расширяет поверхность атаки (по умолчанию выключено);
|
|
- semver: при несовместимости контракта (порты, бэкап, поведение без флага) —
|
|
возможен major `2.x`; решение после реализации.
|
|
|
|
**Внешняя зависимость деплоя:** опциональный antispam-контейнер — вне образа
|
|
SelfPost, поднимается оператором при включении опции.
|
|
|
|
## Зависимости
|
|
|
|
Готовый исходящий тракт (уже реализован). Согласование получено — см. статус
|
|
выше.
|