From 1e638055c8674fc1181a059af52cdca4a37e7b7e Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 22 Mar 2026 20:07:59 +0700 Subject: [PATCH] =?UTF-8?q?feat(mail):=20rename=20mail=E2=86=92mx,=20webma?= =?UTF-8?q?il=E2=86=92mail.csrx.ru=20+=20reliability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename: - docker-mailserver: hostname mail → mx, OVERRIDE_HOSTNAME → mx.csrx.ru - Traefik route: webmail/domain_webmail → mail/domain_mail - domain_webmail removed, domain_mail + domain_mx added to main.yml - certbot cert: mail.csrx.ru → mx.csrx.ru Email reliability improvements: - certbot renewal cron (03:15 + 15:15 daily) - deploy-hook: auto-reload Postfix+Dovecot after cert renewal - POSTFIX_MESSAGE_SIZE_LIMIT=26214400 (25 MB) - SPF hardened: ~all → -all - DMARC hardened: p=none → p=quarantine, added ruf + fo=1 + adkim/aspf strict - autodiscover/autoconfig CNAME records for mail client setup - dns-zone.zone fully updated with architecture comments Docs: - STATUS.md: full mail architecture section, client settings, DNS table - BACKLOG.md: rDNS task + DNS migration steps - DECISIONS.md: mx/mail split rationale Co-Authored-By: Claude Sonnet 4.6 --- dns-zone.zone | 45 ++++++++++++++----- docs/BACKLOG.md | 21 ++++++++- docs/DECISIONS.md | 12 ++++- docs/STATUS.md | 40 ++++++++++++++--- inventory/group_vars/all/main.yml | 3 +- .../templates/traefik/dynamic/routes.yml.j2 | 8 ++-- roles/tools/tasks/main.yml | 29 +++++++++++- roles/tools/templates/docker-compose.yml.j2 | 17 +++---- 8 files changed, 138 insertions(+), 37 deletions(-) diff --git a/dns-zone.zone b/dns-zone.zone index 1e33122..c3d2938 100644 --- a/dns-zone.zone +++ b/dns-zone.zone @@ -1,23 +1,44 @@ +; DNS zone for csrx.ru — reference file, apply manually in Cloudflare +; Last updated: 2026-03-22 +; +; Architecture: +; mail.csrx.ru → 87.249.49.32 (Cloudflare proxied) → Traefik → SnappyMail webmail +; mx.csrx.ru → 85.193.83.9 (DNS-only, NOT proxied) → docker-mailserver SMTP/IMAP +; $ORIGIN csrx.ru. $TTL 3600 -; ── A-записи сервисов ──────────────────────────────────────────────────────── +; ── A-записи сервисов (Cloudflare proxied) ─────────────────────────────────── vault IN A 87.249.49.32 git IN A 87.249.49.32 plane IN A 87.249.49.32 -sync IN A 87.249.49.32 traefik IN A 87.249.49.32 -mail IN A 87.249.49.32 +dash IN A 87.249.49.32 +auth IN A 87.249.49.32 +status IN A 87.249.49.32 +wiki IN A 87.249.49.32 +n8n IN A 87.249.49.32 +mail IN A 87.249.49.32 ; SnappyMail webmail (via Traefik, proxied) -; ── Почта ──────────────────────────────────────────────────────────────────── -@ IN MX 10 mail.csrx.ru. +; ── A-записи прямого подключения (DNS-only, Cloudflare proxy OFF) ───────────── +mx IN A 85.193.83.9 ; docker-mailserver MX/SMTP/IMAP — НЕ проксировать! -; SPF — разрешаем отправку только с нашего mail-сервера -@ IN TXT "v=spf1 mx ~all" +; ── Почта ───────────────────────────────────────────────────────────────────── +; MX — входящая почта идёт на mx.csrx.ru +@ IN MX 10 mx.csrx.ru. -; DMARC — мониторинг без блокировки (p=none), отчёты на admin@csrx.ru -_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:admin@csrx.ru" +; SPF — разрешаем отправку только с IP из MX-записи (85.193.83.9) +; mx = "IP-адреса из всех MX-записей" = mx.csrx.ru = 85.193.83.9 +@ IN TXT "v=spf1 mx -all" -; DKIM — добавить после первого запуска Stalwart (взять ключ из mail.csrx.ru → DKIM) -; Пример как будет выглядеть: -; mail._domainkey IN TXT "v=DKIM1; k=rsa; p=<ключ из Stalwart>" +; DMARC — режим quarantine (подозрительные письма в спам), отчёты на admin@ +_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:admin@csrx.ru; ruf=mailto:admin@csrx.ru; fo=1; adkim=s; aspf=s" + +; DKIM — selector "mail", ключ генерируется при первом деплое docker-mailserver +; Взять из: cat /opt/tools/mailserver/config/opendkim/keys/csrx.ru/mail.txt +; mail._domainkey IN TXT "v=DKIM1; k=rsa; p=" + +; ── Autodiscover / Autoconfig (для почтовых клиентов) ───────────────────────── +; Thunderbird, Outlook автоматически находят настройки сервера +autoconfig IN CNAME mx.csrx.ru. +autodiscover IN CNAME mx.csrx.ru. diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index f6cfe96..66bdd77 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -7,7 +7,20 @@ ## 🔴 Критично (сделать как можно скорее) -- [ ] **Добавить DNS-запись `dash.csrx.ru`** в Cloudflare +- [ ] **rDNS (PTR-запись) для 85.193.83.9** в панели Timeweb + Установить: `85.193.83.9 → mx.csrx.ru` + Без PTR Gmail/Yandex будут отклонять или помечать наши письма как спам. + Путь: Timeweb → Cloud VPS → tools-сервер → Сеть → Обратная DNS-запись + +- [ ] **Обновить DNS в Cloudflare** после деплоя: + - Удалить старую A `mail` → 85.193.83.9 (DNS-only) + - Добавить A `mx` → 85.193.83.9 (DNS-only, orange cloud OFF) + - Изменить MX запись: `csrx.ru MX 10 mx.csrx.ru.` + - Обновить SPF: `v=spf1 mx -all` + - Обновить DMARC: `v=DMARC1; p=quarantine; rua=mailto:admin@csrx.ru; ...` + - Добавить CNAME `autoconfig` → `mx.csrx.ru` + - Добавить CNAME `autodiscover` → `mx.csrx.ru` + - A `mail` → 87.249.49.32 (proxied, уже есть — оставить) A `dash` → `87.249.49.32` (proxied). Grafana сейчас недоступна по домену. - [ ] **Бэкап tools-сервера** @@ -83,7 +96,11 @@ - [x] Outline wiki с email magic link авторизацией - [x] n8n автоматизация - [x] docker-mailserver (Postfix + Dovecot), аккаунты: noreply, admin, jack -- [x] SnappyMail вебмейл на webmail.csrx.ru +- [x] SnappyMail вебмейл, переименован на mail.csrx.ru (было webmail.csrx.ru) +- [x] docker-mailserver переименован на mx.csrx.ru (было mail.csrx.ru) +- [x] Certbot авторотация сертификата (cron 2x/день + deploy-hook для перезагрузки Postfix/Dovecot) +- [x] DMARC ужесточён до p=quarantine (было p=none) +- [x] SPF ужесточён до -all (было ~all) - [x] DKIM/SPF/DMARC DNS-записи для почты - [x] Мониторинг (Prometheus + Grafana + Loki + AlertManager) - [x] CrowdSec IDS + fail2ban diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index ebed4ca..bde0ee1 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -80,10 +80,20 @@ ### SnappyMail вместо Roundcube **Дата:** 2026-03 -**Решение:** djmaze/snappymail как веб-клиент почты. +**Решение:** djmaze/snappymail как веб-клиент почты на `mail.csrx.ru`. **Причина:** Лёгкий, современный UI, простой Docker образ. **Альтернативы:** Roundcube (тяжелее, требует MySQL), Rainloop (заброшен). +### mx.csrx.ru вместо mail.csrx.ru для MX-сервера +**Дата:** 2026-03 +**Решение:** docker-mailserver hostname = `mx.csrx.ru`, веб-клиент = `mail.csrx.ru`. +**Причина:** Стандарт индустрии — MX-хост называется `mx`, пользовательский интерфейс — `mail`. +Раньше оба указывали на разные вещи под одним именем, что запутывало. +**DNS:** `mx.csrx.ru` → 85.193.83.9 (DNS-only, прямые порты SMTP/IMAP). +`mail.csrx.ru` → 87.249.49.32 (Cloudflare proxied, HTTPS вебмейл через Traefik). +**SPF:** `v=spf1 mx -all` — `-all` жёстче чем `~all`, явно запрещает чужие отправители. +**DMARC:** `p=quarantine` — подозрительные письма в спам (было `p=none` — только мониторинг). + --- ## CI/CD diff --git a/docs/STATUS.md b/docs/STATUS.md index 9bf39f1..1e38140 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -41,22 +41,48 @@ | Outline | wiki.csrx.ru | ✅ | Wiki, аутентификация через email magic link | | n8n | n8n.csrx.ru | ✅ | Автоматизация workflow | | docker-mailserver | mail.csrx.ru | ✅ | Postfix + Dovecot, порты 25/465/587/993 | -| SnappyMail | webmail.csrx.ru | ✅ | Веб-клиент почты | +| SnappyMail | mail.csrx.ru | ✅ | Веб-клиент почты (ранее webmail.csrx.ru) | ### Почта (@csrx.ru) +**Аккаунты:** + | Аккаунт | Назначение | |---------|-----------| | noreply@csrx.ru | Системные письма (Outline magic link) | | admin@csrx.ru | Администратор | | jack@csrx.ru | Личный | -**DNS-записи для почты:** -- A `mail.csrx.ru` → 85.193.83.9 -- MX `csrx.ru` → `mail.csrx.ru` (priority 10) -- TXT `csrx.ru` → `v=spf1 mx ~all` -- TXT `_dmarc.csrx.ru` → `v=DMARC1; p=quarantine; rua=mailto:admin@csrx.ru` -- TXT `mail._domainkey.csrx.ru` → DKIM-ключ (генерируется автоматически при первом деплое) +**Архитектура почты:** +``` +Входящая: internet → port 25 → mx.csrx.ru (85.193.83.9) → Postfix → Dovecot → IMAP +Исходящая: Outlook/SnappyMail → port 587 (STARTTLS) → mx.csrx.ru → Postfix → internet +Outline: outline контейнер → mailserver:587 (Docker mail-internal сеть) → noreply@ +``` + +**Настройки почтового клиента:** +- IMAP: `mx.csrx.ru` порт 993 (SSL/TLS) +- SMTP: `mx.csrx.ru` порт 587 (STARTTLS) или 465 (SSL/TLS) +- Веб-клиент: https://mail.csrx.ru (SnappyMail) + +**DNS-записи для почты** (все должны быть в Cloudflare): +``` +A mx → 85.193.83.9 DNS-only (НЕ proxied!) +A mail → 87.249.49.32 Proxied (webmail через Traefik) +MX @ 10 → mx.csrx.ru. +TXT @ → "v=spf1 mx -all" +TXT _dmarc → "v=DMARC1; p=quarantine; rua=mailto:admin@csrx.ru; ..." +TXT mail._domainkey → "v=DKIM1; k=rsa; p=" (генерируется при деплое) +CNAME autoconfig → mx.csrx.ru. для Thunderbird autodiscover +CNAME autodiscover → mx.csrx.ru. для Outlook autodiscover +``` + +**rDNS (PTR-запись)** — настроить в панели Timeweb: +`85.193.83.9 → mx.csrx.ru` (критично для доставки в Gmail/Yandex!) + +**Автообновление TLS-сертификата:** +- certbot renew cron: каждый день в 03:15 и 15:15 +- deploy-hook: после обновления автоматически перезагружает Postfix+Dovecot --- diff --git a/inventory/group_vars/all/main.yml b/inventory/group_vars/all/main.yml index e9642fc..8daf598 100644 --- a/inventory/group_vars/all/main.yml +++ b/inventory/group_vars/all/main.yml @@ -12,7 +12,8 @@ domain_auth: "auth.{{ domain_base }}" domain_status: "status.{{ domain_base }}" domain_wiki: "wiki.{{ domain_base }}" domain_n8n: "n8n.{{ domain_base }}" -domain_webmail: "webmail.{{ domain_base }}" +domain_mail: "mail.{{ domain_base }}" # SnappyMail webmail (HTTPS via Traefik) +domain_mx: "mx.{{ domain_base }}" # docker-mailserver FQDN (SMTP/IMAP direct) # Service paths services_root: /opt/services diff --git a/roles/services/templates/traefik/dynamic/routes.yml.j2 b/roles/services/templates/traefik/dynamic/routes.yml.j2 index a7c6ea1..8580375 100644 --- a/roles/services/templates/traefik/dynamic/routes.yml.j2 +++ b/roles/services/templates/traefik/dynamic/routes.yml.j2 @@ -114,12 +114,12 @@ http: service: n8n middlewares: [rate-limit-strict] - webmail: - rule: "Host(`{{ domain_webmail }}`)" + mail: + rule: "Host(`{{ domain_mail }}`)" entrypoints: [websecure] tls: certresolver: letsencrypt - service: webmail + service: mail middlewares: [rate-limit-default] services: @@ -179,7 +179,7 @@ http: servers: - url: "http://{{ ip_tools }}:5678" - webmail: + mail: loadBalancer: servers: - url: "http://{{ ip_tools }}:8888" diff --git a/roles/tools/tasks/main.yml b/roles/tools/tasks/main.yml index 1efb8b1..efa8c19 100644 --- a/roles/tools/tasks/main.yml +++ b/roles/tools/tasks/main.yml @@ -46,14 +46,14 @@ owner: root group: root -- name: Obtain TLS certificate for mail.{{ domain_base }} +- name: Obtain TLS certificate for mx.{{ domain_base }} ansible.builtin.command: > certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini --email {{ acme_email }} --agree-tos --no-eff-email - -d mail.{{ domain_base }} + -d mx.{{ domain_base }} --non-interactive register: certbot_result changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout" @@ -62,6 +62,31 @@ - "'Certificate not yet due for renewal' not in certbot_result.stdout" - "'Certificate not yet due for renewal' not in certbot_result.stderr" +- name: Deploy certbot renewal deploy-hook (reload mailserver after cert renewal) + ansible.builtin.copy: + dest: /etc/letsencrypt/renewal-hooks/deploy/reload-mailserver.sh + mode: "0750" + owner: root + group: root + content: | + #!/usr/bin/env bash + # Triggered by certbot after successful cert renewal + # Reloads Postfix + Dovecot TLS without full restart + set -euo pipefail + if docker ps --format '{{ '{{' }}.Names{{ '}}' }}' | grep -q '^mailserver$'; then + docker exec mailserver supervisorctl restart postfix dovecot + echo "[$(date)] mailserver TLS reloaded after cert renewal" + fi + +- name: Schedule certbot auto-renewal (twice daily, certbot renews only when needed) + ansible.builtin.cron: + name: "certbot renew" + minute: "15" + hour: "3,15" + job: "certbot renew --quiet --deploy-hooks 2>&1 | logger -t certbot" + user: root + state: present + # ── Open mail ports in UFW ──────────────────────────────────────────────────── - name: Allow SMTP inbound (port 25) community.general.ufw: diff --git a/roles/tools/templates/docker-compose.yml.j2 b/roles/tools/templates/docker-compose.yml.j2 index c4fd609..e4b1670 100644 --- a/roles/tools/templates/docker-compose.yml.j2 +++ b/roles/tools/templates/docker-compose.yml.j2 @@ -138,26 +138,27 @@ services: mailserver: image: {{ mailserver_image }} container_name: mailserver - hostname: mail + hostname: mx domainname: {{ domain_base }} restart: unless-stopped networks: - - mail-internal # Outline → mailserver (internal, port 25, no auth) + - mail-internal # Outline → mailserver (internal, port 587 with auth) - front # inbound/outbound internet SMTP ports: - "{{ ip_tools }}:25:25" # SMTP inbound (MX delivery from internet) - - "{{ ip_tools }}:587:587" # SMTP submission (mail clients) - - "{{ ip_tools }}:993:993" # IMAPS (mail clients) - - "{{ ip_tools }}:465:465" # SMTPS (mail clients, alternative) + - "{{ ip_tools }}:587:587" # SMTP submission (mail clients, STARTTLS) + - "{{ ip_tools }}:993:993" # IMAPS (mail clients, TLS) + - "{{ ip_tools }}:465:465" # SMTPS (mail clients, implicit TLS) environment: - - ENABLE_RSPAMD=1 # spam filter for inbound mail + - ENABLE_RSPAMD=1 # spam filter + DKIM signing - ENABLE_CLAMAV=0 # no antivirus (saves RAM) - ENABLE_FAIL2BAN=0 # host fail2ban already handles this - POSTFIX_INET_PROTOCOLS=ipv4 - - SSL_TYPE=letsencrypt # TLS via certbot cert at /etc/letsencrypt + - SSL_TYPE=letsencrypt # TLS certs from /etc/letsencrypt/live/mx.csrx.ru/ - LOG_LEVEL=warn - - OVERRIDE_HOSTNAME=mail.{{ domain_base }} + - OVERRIDE_HOSTNAME=mx.{{ domain_base }} - POSTMASTER_ADDRESS=admin@{{ domain_base }} + - POSTFIX_MESSAGE_SIZE_LIMIT=26214400 # 25 MB max message size volumes: - {{ tools_root }}/mailserver/mail-data:/var/mail - {{ tools_root }}/mailserver/mail-state:/var/mail-state