diff --git a/docs/progress.md b/docs/progress.md index 33e465f..21a21cb 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -46,10 +46,20 @@ ## Текущее состояние -- **Текущая фаза:** 4 ✅ закрыта → следующая **Фаза 5** (полная конфигурация Postfix — исходящий релей) -- **Модель для Фазы 5:** Opus (самый чувствительный тракт доставки: SASL/TLS на 465, `reject_sender_login_mismatch`, отсутствие open relay) -- **Статус:** приложения + SASL + привязка к домену реализованы и проверены на сервере (`gofmt`/`go vet`/`go test` зелёные, docker-образ собирается; контейнерный e2e: создание wildcard/list-приложений с показом пароля один раз, валидация (чужой домен → 400, дубль логина → 409, `@` в логине → 400), учётки в `sasldb2` под realm, генерация `smtpd_sender_login_maps` с many-to-one слиянием логинов, перевыпуск пароля, смена режима, удаление приложения, каскадное удаление домена с очисткой `sasldb2` и пересборкой карты, персистентность `sasldb2`+карты через рестарт с самолечением прав, **реальный `postfix reload`** через одноразовую supervisord-программу) -- **Следующий шаг (Фаза 5):** `master.cf` — `smtps` 465 (wrapper TLS) как основной, опционально `submission` 587 (STARTTLS, `smtpd_tls_auth_only=yes`); `main.cf` — `smtpd_sasl_auth_enable`, `smtpd_sasl_type=cyrus` + `smtpd/sasl` conf, указывающий на `/data/sasl/sasldb2`; **`smtpd_sender_login_maps = texthash:/data/postfix/sender_login_maps`** (файл уже генерируется панелью в Фазе 4) + `reject_sender_login_mismatch` в `smtpd_sender_restrictions`; `smtpd_recipient_restrictions` (`permit_sasl_authenticated`, `reject_unauth_destination`), никакого open relay; TLS cert/key из `TLS_CERT_FILE`/`TLS_KEY_FILE` (read-only mount) + периодический reload; исходящая доставка (MX-lookup, `smtp_tls_security_level=may`); rate-limit уровня 1 (`anvil`); milter-цепочка OpenDKIM + journal (fail-open). **Критично для Фазы 5:** определиться с реалмом SASL — панель создаёт учётки под realm `SELFPOST_HOSTNAME` (см. `SASL_REALM`); значение в карте — «голый» логин. Настроить Postfix так, чтобы `sasl_username` совпадал со значением карты (`smtpd_sasl_local_domain` пустой ИЛИ подгонка realm), и проверить реальной отправкой. Также решить chroot для `smtpd`: `/data/sasl/sasldb2` вне `/var/spool/postfix` — либо отключить chroot для сервиса приёма, либо иначе обеспечить доступ. +- **Текущая фаза:** **Фаза 5** — код завершён, закоммичен (`b2692e4`) и проверен на сервере; ⏳ ждём **единственного** финального подтверждения — реальной доставки в ящик с `dkim=pass` (упирается в кэш DNS у получателя, см. ниже). После подтверждения → закрыть Фазу 5, следующая **Фаза 6** (journal-milter) на **Opus**. +- **Что осталось:** фоновый цикл повторов на сервере (`/tmp/p5retry.sh`, лог `/root/p5retry.log`, контейнер `p5` держать живым — его ключ mixeme.ru опубликован) шлёт `selfpost@mixeme.ru → selfpost@mixeme.ru` каждые ~25 мин и остановится, как только `mc.mixfed.ru` примет письмо. Приёмник пока отбивает `554 does not meet our delivery requirements`: наш DKIM валиден (`opendkim-testkey: key OK`), SPF теперь авторизует наш IP `81.30.105.2`, но у зоны mixeme.ru negative-TTL 3ч (SOA min 10800) и старый SPF TTL 24ч — резолвер получателя, вероятно, ещё отдаёт до-публикационные записи. Альтернатива, если это не кэш, а политика «own-domain» приёмника: отправить на нейтральный ящик (Gmail) — примет по DKIM/DMARC-выравниванию. **Со стороны SelfPost всё доказано** (см. «Сделано в Фазе 5»); незакрытый пункт — свойство приёмника, не дефект релея. + +### Сделано в Фазе 5 +- **Генерация конфига релея из env** (`build/postfix-config.sh`, вызывается из `entrypoint.sh` под root на каждом старте — не в образе, чтобы cert-пути/лимиты/hostname/587 задавались env и пересобирались как остальное состояние). `main.cf` через `postconf -e`, сервисы `master.cf` через `postconf -M/-P`, финальный `postfix check`. +- **`master.cf`:** `smtps` 465 (implicit TLS, `smtpd_tls_wrappermode=yes`) — основной; `submission` 587 (STARTTLS, `smtpd_tls_security_level=encrypt`) — опционально по `SUBMISSION_ENABLE=true` (иначе удаляется `postconf -MX`). Оба `chroot=n` + `smtpd_client_restrictions=permit_sasl_authenticated,reject`. +- **SASL:** `smtpd_sasl_type=cyrus`, conf `/etc/postfix/sasl/smtpd.conf` (`auxprop`/`sasldb`, `sasldb_path=/data/sasl/sasldb2`). **Рабочий реалм:** `smtpd_sasl_local_domain` пустой + `myhostname=$SELFPOST_HOSTNAME` → клиент логинится «голым» логином, Cyrus резолвит по realm=myhostname (= realm учётки), а `sasl_username` для карты = голый логин = значение карты. Проверено (235 + отправка). +- **Привязка/анти-релей:** `smtpd_sender_login_maps=texthash:/data/postfix/sender_login_maps` + `reject_sender_login_mismatch`; `smtpd_relay_restrictions`/`smtpd_recipient_restrictions=permit_sasl_authenticated, reject_unauth_destination`; **нет `permit_mynetworks`** — авторизация только по кредам. Open relay невозможен. +- **TLS/доставка:** cert/key из `TLS_CERT_FILE`/`TLS_KEY_FILE`; `smtp_tls_security_level=may` (исходящая, MX-lookup напрямую); суточный `postfix reload` для подхвата обновлённых сертов (`build/postfix-cert-reload.sh`, `[program:cert-reload]` под supervisord, exit≠0 не роняет контейнер). +- **Rate-limit L1:** `smtpd_client_message_rate_limit`/`anvil_rate_time_unit` из env (дефолт 100/3600с). +- **Milter-цепочка (per-milter действия, синтаксис скобок Postfix 3.0):** `{ unix:opendkim.sock, default_action=tempfail }, { unix:journal.sock, default_action=accept }` — OpenDKIM строго (при недоступности → defer), journal fail-open (не блокирует релей, ТЗ 7.3). `non_smtpd_milters` пустой. +- **Два инфра-фикса, найденных на сервере** (см. память [[postfix-chroot-and-milter-socket-perms]]): (1) `postconf -F '*/*/chroot=n'` — chroot-агент доставки не читает `/etc/resolv.conf`, MX-lookup падал «Host not found», письма не уходили; (2) `entrypoint.sh` ставит `/run/opendkim` и `/run/selfpost` в группу `selfpost`+setgid `2750`, journal-stub делает `chmod 0660` на сокет — иначе `postfix` не мог подключиться к milter-сокетам, а строгий OpenDKIM отбивал всю почту (`milter-reject` 451). Journal-stub (`cmd/panel/journalstub.go`) — единственное изменение Go. +- **Проверено на сервере** (selfpost.mixfed.ru): `gofmt`/`vet`/`test` зелёные, образ собирается; контейнерный e2e — 465 auth+отправка **подписано DKIM** (`d=домен; s=selfpost`), 587 STARTTLS auth, кросс-доменный отправитель `553 not owned`, list-режим по адресам, неаутентифицированный релей `554`, **реальная исходящая доставка до MX получателя по TLS** (`relay=mc.mixfed.ru`), DKIM-подпись верифицируется опубликованным ключом (`opendkim-testkey: key OK`). +- **Не закрыто:** финальный приём письма в ящик получателем `mc.mixfed.ru` (кэш DNS / own-domain-политика приёмника — не дефект SelfPost). Автоповтор запущен. ### Сделано в Фазе 4 - **Модель приложения** (`internal/store/applications.go`): CRUD над `applications`/`application_addresses` в транзакциях; `login` глобально уникален (арбитр — UNIQUE, `ErrLoginExists`); `ListBindings` (SQL `UNION ALL`) отдаёт пары «адрес→логин» (wildcard → `@domain`, list → каждый адрес) детерминированно отсортированными — сырьё для карты; `ListLoginsByDomain` для очистки `sasldb2` перед каскадом; `DeleteApplication` возвращает удалённую строку (нужен логин для `sasldb2`).