Почтовый сервер Postfix на Debian 13: Dovecot 2.4, Rspamd, DKIM и панель Mailroom

обновлено 83 мин проверено на Debian 13 (Trixie), Postfix 3.10, Dovecot 2.4.1, PostgreSQL 17
Содержание
  1. Что изменилось по сравнению со старой версией
  2. Схема работы
  3. Значения в примерах
  4. 1. Подготовка сервера и DNS
  5. DNS
  6. Имя и время
  7. 2. Установка пакетов
  8. Резервная копия пакетной конфигурации
  9. 3. Пользователь и каталог хранения
  10. 4. База данных и разделение доступа
  11. 5. PostfixAdmin
  12. Панель — только через SSH-туннель
  13. 6. Сертификат TLS
  14. Автоматическое обновление
  15. 7. Postfix: основная конфигурация
  16. Служебные письма
  17. 8. SQL-карты Postfix
  18. 9. Отправка клиентами: порты 587 и 465
  19. 10. Dovecot 2.4: IMAPS, SASL и LMTP
  20. Квоты
  21. POP3 — только при необходимости
  22. 11. Rspamd: антиспам и DKIM
  23. Ключ DKIM
  24. 12. Первый запуск и firewall
  25. 13. Проверка работы
  26. TLS
  27. Аутентификация
  28. Отправка и доставка
  29. Проверка на open relay
  30. Настройки почтового клиента
  31. 14. Логи, очередь и типичные ошибки
  32. 15. Защита от перебора и эксплуатация
  33. Резервное копирование
  34. 16. Перенос существующего сервера
  35. 17. Альтернатива: системные пользователи Linux
  36. 18. LDAP: что решить до настройки
  37. 19. Если нужен именно шлюз перед другим сервером
  38. 20. Итоговый чек-лист перед вводом в эксплуатацию
  39. 21. Автоматическая установка: Mailstack
  40. Требования
  41. Запуск
  42. Что создаётся
  43. После установки — обязательно
  44. Обслуживание
  45. Если установка прервалась
  46. Что проверено
  47. 22. Mailroom: своя панель управления и веб-почта
  48. Установка
  49. Как устроена
  50. Панель администратора
  51. Эксплуатация
  52. Веб-почта
  53. Календарь и контакты
  54. Кому подойдёт
  55. Источники

Полностью переработанная версия статьи о Postfix для Debian 13 (Trixie). Основной сценарий — самостоятельный почтовый сервер: виртуальные домены и ящики, база PostgreSQL или MariaDB, PostfixAdmin, IMAPS, защищённая отправка, Rspamd и DKIM.

В конце статьи — автоматический установщик Mailstack (install.sh). Он разворачивает расширенный стек: веб-почта Roundcube, Sieve-фильтры и автоответы, ClamAV, лимиты отправки, проверки состояния и шифрованное резервное копирование. СУБД выбирается при установке: PostgreSQL (по умолчанию) или MariaDB. Ручная установка и установщик — альтернативы, выполнять оба на одном сервере нельзя.

В разделе 22 — Mailroom: собственная панель управления и веб-почта для этой схемы. Она объединяет управление доменами и ящиками, мастер DNS с проверкой DKIM, SPF и DMARC, рекомендации, очередь, трассировку писем, резервные копии, оповещения, веб-почту, календарь и контакты.

Что изменилось по сравнению со старой версией

Прежняя статья была написана для FreeBSD и Dovecot 2.3. Что исправлено:

  • Команды FreeBSD (portsnap, make config, rc.conf) заменены пакетами Debian и systemd. Пути — /etc/postfix, /etc/dovecot, /etc/postfixadmin.
  • Конфигурация Dovecot переписана для ветки 2.4. Старые mail_location, disable_plaintext_auth, переменные %u/%d/%n и безымянные блоки passdb/userdb больше не работают; обязательны dovecot_config_version и dovecot_storage_version.[1]
  • SMTP-аутентификацию выполняет Dovecot. Cyrus SASL и saslauthd не нужны.
  • Письма виртуальным пользователям доставляются по LMTP через закрытый UNIX-сокет, а не внешним pipe-процессом.
  • Убраны права 777, сокеты, доступные всем пользователям, MD5-пароли и RSA-ключи 1024 бит. Пароли ящиков хранятся в ARGON2ID.
  • Приём почты на порту 25 отделён от отправки клиентами на 587/465. Для клиентов TLS и авторизация обязательны.
  • Добавлены DNS, PTR, SPF, DKIM, DMARC, обновление сертификатов, квоты, диагностика и резервное копирование.
  • Добавлен выбор СУБД: PostgreSQL или MariaDB. SQL-запросы карт Postfix одинаковы для обеих.
  • Уточнены роли: Postfix принимает и отправляет письма по SMTP, Dovecot отдаёт ящики по IMAP/POP3 и доставляет письма в них по LMTP. Ответ SMTP queued as … означает постановку в очередь, а не доставку получателю.

Обещанная в старой версии настройка LDAP так и не была написана. В разделе о LDAP перечислено, что нужно решить до неё.

Схема работы

текст
Входящее письмо:
Интернет → Postfix :25 → Rspamd → Dovecot LMTP → Maildir

Отправка пользователем:
Почтовый клиент → Postfix :587 STARTTLS / :465 TLS
                  ↓ SASL через Dovecot → база (PostgreSQL / MariaDB)
                Rspamd (проверка + DKIM-подпись) → Интернет

Чтение почты:
Почтовый клиент → Dovecot :993 TLS → Maildir

Управление:
Администратор → PostfixAdmin → база

Сервер с собственными ящиками — не только «шлюз». Настоящий шлюз принимает письма и пересылает их на другой сервер, например Exchange. Такой вариант описан отдельно в разделе «Если нужен именно шлюз». Эти две конфигурации нельзя механически объединять.

Значения в примерах

Параметр Значение в статье
Почтовый домен example.com
Имя сервера mail.example.com
Публичный IPv4 203.0.113.10 — документационный адрес, не рабочий
Проверочный ящик alice@example.com
Хранилище писем /var/vmail
Служебный пользователь vmail, UID/GID 5000
База данных postfixadmin
Пользователь БД приложения pfa
Пользователь БД только для чтения mailreader

Все команды выполняются от root (sudo -i), если не сказано иное. В почтовом клиенте логином служит полный адрес ящика.

1. Подготовка сервера и DNS

Нужны статический публичный IPv4, управление DNS, возможность задать PTR у провайдера и открытый TCP/25 в обе стороны. Если исходящий 25 заблокирован, отправлять придётся через SMTP-relay провайдера. Если заблокирован входящий 25, приём почты из Интернета без внешнего шлюза невозможен.

Почтовый сервер лучше держать на отдельной машине. MX не должен указывать на HTTP-прокси или CDN: SMTP нужен прямой адрес.

DNS

dns
example.com.        IN MX  10 mail.example.com.
mail.example.com.   IN A   203.0.113.10
example.com.        IN TXT "v=spf1 ip4:203.0.113.10 -all"
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
  • SPF с -all подходит, только если этот IP — единственный источник отправки. Если письма уходят ещё через рассылки, CRM или relay, перечислите их в одной записи v=spf1. Две SPF-записи на одном имени — ошибка.
  • MX указывает на имя с A-записью, не на CNAME.
  • AAAA публикуйте только при полностью работающем IPv6 (маршрутизация, firewall, PTR). В примерах IPv6 для Postfix выключен намеренно.
  • DMARC начинайте с p=none, создайте ящик или алиас dmarc@example.com, изучите отчёты и проверьте все легальные источники отправки. Затем переходите к quarantine и reject. DMARC проходит, когда домен видимого From: совпадает с доменом, успешно прошедшим SPF или DKIM, — само наличие записей ничего не гарантирует.

У провайдера установите PTR: 203.0.113.10 → mail.example.com. Прямая A-запись должна возвращать тот же адрес.

Имя и время

bash
hostnamectl set-hostname mail.example.com
apt update && apt upgrade
apt install systemd-timesyncd ca-certificates curl dnsutils openssl nano
systemctl enable --now systemd-timesyncd
timedatectl status
hostname -f

Если уже работает chrony или другой NTP-клиент, оставьте его: два клиента времени не нужны. Не подменяйте публичное имя сервера записью 127.0.0.1 в /etc/hosts.

bash
dig +short MX example.com
dig +short A mail.example.com
dig +short -x 203.0.113.10
dig +short TXT example.com

2. Установка пакетов

Выберите СУБД. Набор пакетов отличается только драйверами базы:

PostgreSQL

bash
apt install postgresql postfix postfix-pgsql \
  dovecot-core dovecot-imapd dovecot-lmtpd dovecot-pgsql \
  apache2 libapache2-mod-php php-cli php-pgsql php-mbstring \
  postfixadmin certbot swaks rsyslog

MariaDB

bash
apt install mariadb-server postfix postfix-mysql \
  dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql \
  apache2 libapache2-mod-php php-cli php-mysql php-mbstring \
  postfixadmin certbot swaks rsyslog

В диалоге Postfix выберите Internet Site, почтовое имя — example.com. Если на сервере есть Exim, проверьте, что apt предлагает его удалить: два MTA не нужны.

В диалоге PostfixAdmin откажитесь от автоматической настройки БД через dbconfig-common: базу создадим вручную. Если она уже создана автоматически, не создавайте вторую схему с другими учётными данными — сначала разберитесь с /etc/postfixadmin/dbconfig.inc.php.

bash
postconf mail_version
dovecot --version
apt-cache policy postfix dovecot-core postfixadmin
systemctl enable --now apache2 rsyslog
systemctl stop postfix dovecot

В Debian 13 используется Dovecot 2.4. Ориентируйтесь на фактически установленные версии, а не на номер из статьи.

На время настройки закройте SMTP/IMAP на внешнем firewall: сервисы с промежуточной конфигурацией не должны быть доступны из Интернета.

Резервная копия пакетной конфигурации

bash
umask 077
BACKUP="/root/mail-config-$(date +%F-%H%M%S)"
mkdir -p "$BACKUP"
cp -a /etc/postfix /etc/dovecot /etc/postfixadmin /etc/apache2 "$BACKUP/"

3. Пользователь и каталог хранения

Проверьте, что UID/GID 5000 свободны:

bash
getent passwd 5000; getent group 5000
getent passwd vmail; getent group vmail

Если заняты — выберите другие значения и замените их во всех настройках ниже.

bash
groupadd --gid 5000 vmail
useradd --uid 5000 --gid vmail --home-dir /var/vmail \
  --no-create-home --shell /usr/sbin/nologin vmail
install -d -o vmail -g vmail -m 0750 /var/vmail

Пароль vmail не нужен. Он владеет письмами всех виртуальных ящиков; учётные записи Linux для ящиков не создаются. Никаких chmod -R 777.

4. База данных и разделение доступа

Сгенерируйте два разных длинных пароля (openssl rand -hex 32) и сохраните их в менеджере паролей. Hex-пароль удобен тем, что его не нужно экранировать в конфигурационных файлах. Ниже PFA_DB_PASSWORD и MAIL_DB_PASSWORD — маркеры, а не значения.

PostfixAdmin нужны права на изменение таблиц и обновление собственной схемы. Postfix и Dovecot достаточно SELECT. Суперпользователя БД этим сервисам не давайте.

PostgreSQL

bash
# Debian по умолчанию слушает только localhost — проверьте:
runuser -u postgres -- psql -Atc 'SHOW listen_addresses'
ss -lntp | grep ':5432'
# SQL-консоль без истории
runuser -u postgres -- env PSQL_HISTORY=/dev/null psql

MariaDB

bash
# В /etc/mysql/mariadb.conf.d/50-server.cnf должно быть bind-address = 127.0.0.1
systemctl restart mariadb
ss -lntp | grep ':3306'
# SQL-консоль без истории
MYSQL_HISTFILE=/dev/null mariadb

PostgreSQL

sql
CREATE ROLE pfa LOGIN PASSWORD 'PFA_DB_PASSWORD';
CREATE ROLE mailreader LOGIN PASSWORD 'MAIL_DB_PASSWORD';
CREATE DATABASE postfixadmin OWNER pfa ENCODING 'UTF8' TEMPLATE template0;
REVOKE ALL ON DATABASE postfixadmin FROM PUBLIC;
GRANT CONNECT ON DATABASE postfixadmin TO mailreader;
\q

MariaDB

sql
CREATE DATABASE postfixadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pfa'@'127.0.0.1' IDENTIFIED BY 'PFA_DB_PASSWORD';
GRANT ALL PRIVILEGES ON postfixadmin.* TO 'pfa'@'127.0.0.1';
CREATE USER 'mailreader'@'127.0.0.1' IDENTIFIED BY 'MAIL_DB_PASSWORD';
GRANT SELECT ON postfixadmin.* TO 'mailreader'@'127.0.0.1';
EXIT;

В PostgreSQL право SELECT для mailreader выдаётся после создания таблиц — это шаг в конце раздела 5. В MariaDB учётная запись @127.0.0.1 работает при TCP-подключении к 127.0.0.1; не меняйте её на localhost без смены способа подключения.

5. PostfixAdmin

В пакете Debian веб-каталог — /usr/share/postfixadmin/public, настройки — /etc/postfixadmin. Свои параметры кладите в config.local.php: остальные файлы обновляются вместе с пакетом. SQL-примеры из README.Debian написаны для Dovecot 2.3, переносить их без адаптации нельзя.

/etc/postfixadmin/config.local.php:

config.local.phpphp
<?php
$CONF['configured'] = true;
$CONF['default_language'] = 'ru';
$CONF['database_type'] = 'pgsql';      // для MariaDB: 'mysqli'
$CONF['database_host'] = '127.0.0.1';
$CONF['database_user'] = 'pfa';
$CONF['database_password'] = 'PFA_DB_PASSWORD';
$CONF['database_name'] = 'postfixadmin';

$CONF['encrypt'] = 'dovecot:ARGON2ID';
$CONF['dovecotpw'] = '/usr/bin/doveadm -O pw';

$CONF['domain_path'] = 'YES';
$CONF['domain_in_mailbox'] = 'NO';
$CONF['quota'] = 'YES';
$CONF['quota_multiplier'] = '1048576';
$CONF['emailcheck_resolve_domain'] = 'YES';
$CONF['password_validation'] = array('/.{12}/' => 'password_too_short 12');

$CONF['setup_password'] = 'REPLACE_WITH_SETUP_HASH';

quota_multiplier = 1048576 означает, что квота в интерфейсе задаётся в МиБ, а в базе хранится в байтах. В старых инструкциях встречается 1024000 — это не мебибайт, и квоты получаются на 2,3 % меньше ожидаемых.

doveadm -O pw не читает конфигурацию Dovecot с секретами. Проверьте поддержку ARGON2ID и запуск от имени веб-сервера:

bash
doveadm -O pw -l
runuser -u www-data -- /usr/bin/doveadm -O pw -s ARGON2ID

Пароль вводится интерактивно; полученный хеш никуда не публикуйте. Если ARGON2ID недоступен, выберите другую стойкую схему (например, SHA512-CRYPT) одновременно в PostfixAdmin и Dovecot. К MD5 не возвращайтесь.

bash
chown root:www-data /etc/postfixadmin/config.local.php
chmod 0640 /etc/postfixadmin/config.local.php
php -l /etc/postfixadmin/config.local.php

Панель — только через SSH-туннель

На отдельном новом сервере замените /etc/apache2/ports.conf на:

apache
Listen 127.0.0.1:80

Если Apache уже обслуживает сайты, глобальные настройки не трогайте: сделайте отдельный virtual host с HTTPS и доступом только из VPN или с административных IP.

bash
a2disconf postfixadmin
a2dissite 000-default

/etc/apache2/sites-available/mailadmin.conf:

mailadmin.confapache
<VirtualHost 127.0.0.1:80>
    ServerName localhost
    DocumentRoot /usr/share/postfixadmin/public

    <Directory /usr/share/postfixadmin/public>
        Options -Indexes +FollowSymLinks
        AllowOverride None
        Require local
        DirectoryIndex index.php
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/mailadmin-error.log
    CustomLog ${APACHE_LOG_DIR}/mailadmin-access.log combined
</VirtualHost>
bash
a2ensite mailadmin
apache2ctl configtest
systemctl restart apache2
ss -lntp | grep ':80'   # должно быть 127.0.0.1:80, не 0.0.0.0:80

На своём компьютере откройте туннель и перейдите на http://127.0.0.1:8080/setup.php:

bash
ssh -N -L 8080:127.0.0.1:80 admin@mail.example.com

HTTP здесь допустим, потому что трафик идёт внутри SSH. Дальше:

  1. На странице настройки сформируйте хеш setup-пароля и подставьте его вместо REPLACE_WITH_SETUP_HASH.
  2. Создайте схему БД и администратора.
  3. Создайте домен example.com, ящик alice@example.com и алиасы postmaster@, abuse@, dmarc@ на существующий ящик.
  4. Закройте setup внутри virtual host и перезагрузите Apache:
apache
<LocationMatch "^/(setup|upgrade)\.php$">
    Require all denied
</LocationMatch>

Для планового обновления схемы открывайте setup временно и только через административный канал. Наружу публикуется только public, а не весь /usr/share/postfixadmin.

Только для PostgreSQL: когда таблицы созданы, выдайте читателю права. ALTER DEFAULT PRIVILEGES нужен, чтобы таблицы, добавленные будущими обновлениями PostfixAdmin, тоже были доступны:

sql
-- runuser -u postgres -- psql -d postfixadmin
GRANT USAGE ON SCHEMA public TO mailreader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mailreader;
ALTER DEFAULT PRIVILEGES FOR ROLE pfa IN SCHEMA public GRANT SELECT ON TABLES TO mailreader;

Проверьте данные:

sql
SELECT domain, active FROM domain;
SELECT username, maildir, quota, active FROM mailbox;
SELECT address, goto, active FROM alias;

С этими настройками maildir нового ящика — example.com/alice/. Это домашний каталог ящика, сами письма лежат в его подкаталоге Maildir.

6. Сертификат TLS

Нужен публично доверенный сертификат для mail.example.com. Для HTTP-01 в режиме standalone нужны правильная A-запись и доступ извне к TCP/80. Apache у нас слушает только loopback, поэтому на время выпуска его останавливаем:

bash
systemctl stop apache2
certbot certonly --standalone -d mail.example.com
systemctl start apache2
certbot certificates

Если certbot завершился ошибкой, запустите Apache вручную и устраните причину (частая — неверная AAAA-запись). Пути сертификата:

текст
/etc/letsencrypt/live/mail.example.com/fullchain.pem
/etc/letsencrypt/live/mail.example.com/privkey.pem

Каталог может получить суффикс, если сертификат уже выпускался, — берите фактический путь из certbot certificates. Права закрытого ключа не меняйте: Postfix и Dovecot читают его привилегированными процессами.

Самоподписанный сертификат годится только для лаборатории. Для внутреннего домена используйте свой CA или реальное DNS-имя с проверкой DNS-01: публичный сертификат для .loc не выпустить.

Автоматическое обновление

/etc/letsencrypt/renewal-hooks/pre/10-stop-mailadmin:

bash
#!/bin/sh
set -eu
systemctl stop apache2

/etc/letsencrypt/renewal-hooks/post/10-start-mailadmin:

bash
#!/bin/sh
set -eu
systemctl start apache2

/etc/letsencrypt/renewal-hooks/deploy/20-reload-mail (создайте после настройки Postfix и Dovecot):

bash
#!/bin/sh
set -eu
postfix check
systemctl reload postfix
doveconf -n >/dev/null
systemctl reload dovecot

При DNS-01 или webroot останавливать Apache не нужно, pre/post-хуки тогда не создавайте.

bash
chmod 0750 /etc/letsencrypt/renewal-hooks/pre/10-stop-mailadmin \
  /etc/letsencrypt/renewal-hooks/post/10-start-mailadmin \
  /etc/letsencrypt/renewal-hooks/deploy/20-reload-mail
systemctl enable --now certbot.timer
certbot renew --dry-run

Сухой прогон проверяет выпуск, но deploy-хуки в нём не обязательно выполняются — перезагрузку сервисов проверьте отдельно. Срок действия сертификата контролируйте мониторингом.

7. Postfix: основная конфигурация

Пакетный main.cf не перезаписываем: параметры ставим через postconf -e. Переменная MAP задаёт драйвер базы:

PostgreSQL

bash
MAP=pgsql

MariaDB

bash
MAP=mysql
bash
postconf -e 'myhostname = mail.example.com'
postconf -e 'mydomain = example.com'
postconf -e 'myorigin = $mydomain'
postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost'
postconf -e 'inet_interfaces = all'
postconf -e 'inet_protocols = ipv4'
postconf -e 'mynetworks = 127.0.0.0/8'
postconf -e 'relayhost ='
postconf -e 'relay_domains ='
postconf -e 'alias_maps = hash:/etc/aliases'
postconf -e 'alias_database = hash:/etc/aliases'
postconf -e 'recipient_delimiter = +'
postconf -e 'message_size_limit = 26214400'
postconf -e 'mailbox_size_limit = 0'
postconf -e 'disable_vrfy_command = yes'
postconf -e 'smtpd_helo_required = yes'

postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'
postconf -e 'smtpd_recipient_restrictions = reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unlisted_recipient'

postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = private/auth'
postconf -e 'smtpd_sasl_auth_enable = no'
postconf -e 'smtpd_sasl_security_options = noanonymous'
postconf -e 'smtpd_tls_auth_only = yes'

postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem'
postconf -e 'smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem'
postconf -e 'smtpd_tls_protocols = >=TLSv1.2'
postconf -e 'smtpd_tls_mandatory_protocols = >=TLSv1.2'
postconf -e 'smtpd_tls_loglevel = 1'
postconf -e 'smtp_tls_security_level = may'
postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
postconf -e 'smtp_tls_protocols = >=TLSv1.2'

postconf -e "virtual_mailbox_domains = proxy:$MAP:/etc/postfix/sql/domains.cf"
postconf -e "virtual_mailbox_maps = proxy:$MAP:/etc/postfix/sql/mailboxes.cf"
postconf -e "virtual_alias_maps = proxy:$MAP:/etc/postfix/sql/aliases.cf, proxy:$MAP:/etc/postfix/sql/alias-domains.cf"
postconf -e 'virtual_transport = lmtp:unix:private/dovecot-lmtp'
postconf -e "smtpd_sender_login_maps = proxy:$MAP:/etc/postfix/sql/sender-login.cf"

Строки с $MAP в двойных кавычках, остальные — в одинарных, чтобы shell не подставил $mydomain и $myhostname.

Что здесь важно:

  • example.com не входит в mydestination: это виртуальный домен. Иначе письма уйдут в локальную доставку Linux.
  • mynetworks — только loopback. Добавив туда офисную сеть, вы разрешите ей отправку без пароля; это отдельное осознанное решение.
  • Пересылку контролирует smtpd_relay_restrictions.[2]
  • На порту 25 AUTH выключен, он включается только на клиентских портах (раздел 9).
  • На 25 TLS необязательный (may), чтобы не отвергать серверы без STARTTLS. Обязательный TLS для межсерверной почты требует отдельной политики (DANE, MTA-STS).
  • Лимит 25 МиБ относится ко всему письму. Из-за base64 вложение может быть примерно на треть меньше.

Служебные письма

В /etc/aliases в пакете Debian уже есть postmaster: root. Добавьте только перенаправление root на реальный ящик — дубликаты ключей newaliases отметит предупреждением:

текст
root: alice@example.com
bash
newaliases

8. SQL-карты Postfix

bash
install -d -o root -g postfix -m 0750 /etc/postfix/sql

Каждый из пяти файлов начинается с общего блока (пароль замените):

ini
user = mailreader
password = MAIL_DB_PASSWORD
hosts = 127.0.0.1
dbname = postfixadmin

Затем — строка query, одной логической строкой. Запросы одинаковы для PostgreSQL и MariaDB: флаги PostfixAdmin в PostgreSQL логические, в MariaDB целые, а литералы '1'/'0' понимают обе. По той же причине вместо MySQL-функции FIND_IN_SET используется POSITION.

/etc/postfix/sql/domains.cf — основной домен и активные домены-алиасы:

sql
query = SELECT domain FROM domain WHERE domain='%s' AND active='1' AND backupmx='0' UNION SELECT ad.alias_domain FROM alias_domain ad JOIN domain d ON d.domain=ad.target_domain WHERE ad.alias_domain='%s' AND ad.active='1' AND d.active='1' AND d.backupmx='0'

/etc/postfix/sql/mailboxes.cf — подтверждает существование ящика (доставку выполняет Dovecot, путь здесь не нужен):

sql
query = SELECT 1 FROM mailbox m JOIN domain d ON d.domain=m.domain WHERE m.username='%s' AND m.active='1' AND d.active='1' AND d.backupmx='0'

/etc/postfix/sql/aliases.cf:

sql
query = SELECT a.goto FROM alias a JOIN domain d ON d.domain=a.domain WHERE a.address='%s' AND a.active='1' AND d.active='1' AND d.backupmx='0'

/etc/postfix/sql/alias-domains.cf — для alice@alias.example вернёт alice@example.com, только если такой получатель существует. Цепочки доменов-алиасов и catch-all через них не поддерживаются намеренно:

sql
query = SELECT CONCAT('%u','@',ad.target_domain) FROM alias_domain ad JOIN domain d ON d.domain=ad.target_domain WHERE ad.alias_domain='%d' AND ad.active='1' AND d.active='1' AND d.backupmx='0' AND (EXISTS (SELECT 1 FROM mailbox m WHERE m.username=CONCAT('%u','@',ad.target_domain) AND m.active='1') OR EXISTS (SELECT 1 FROM alias a WHERE a.address=CONCAT('%u','@',ad.target_domain) AND a.active='1'))

/etc/postfix/sql/sender-login.cf — кто может отправлять с этого адреса: владелец ящика, а для алиаса — его активные получатели-ящики:

sql
query = SELECT m.username FROM mailbox m JOIN domain d ON d.domain=m.domain WHERE m.username='%s' AND m.active='1' AND d.active='1' AND d.backupmx='0' UNION SELECT m.username FROM alias a JOIN mailbox m ON POSITION(CONCAT(',',m.username,',') IN CONCAT(',',a.goto,','))>0 JOIN domain d ON d.domain=a.domain JOIN domain md ON md.domain=m.domain WHERE a.address='%s' AND a.active='1' AND m.active='1' AND d.active='1' AND md.active='1' AND d.backupmx='0' AND md.backupmx='0'
bash
chown root:postfix /etc/postfix/sql/*.cf
chmod 0640 /etc/postfix/sql/*.cf
postconf -m | grep -E 'pgsql|mysql'
postmap -q example.com              proxy:$MAP:/etc/postfix/sql/domains.cf
postmap -q alice@example.com        proxy:$MAP:/etc/postfix/sql/mailboxes.cf
postmap -q postmaster@example.com   proxy:$MAP:/etc/postfix/sql/aliases.cf
postmap -q alice@example.com        proxy:$MAP:/etc/postfix/sql/sender-login.cf

Ожидаются: домен, 1, адрес назначения и alice@example.com. Неизвестный ящик даёт пустой результат. Отдельно проверьте выключенный ящик, выключенный домен и домен-алиас. Ошибка подключения к БД — не то же самое, что «нет пользователя»: ищите её в логе. postmap без -q для SQL-карт не нужен.

9. Отправка клиентами: порты 587 и 465

В /etc/postfix/master.cf сохраните пакетные службы. Если строки submission, submissions или smtps уже есть, отредактируйте их, а не добавляйте дубли:

master.cfтекст
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=reject_authenticated_sender_login_mismatch

submissions inet n      -       y       -       -       smtpd
  -o syslog_name=postfix/submissions
  -o smtpd_tls_wrappermode=yes
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=reject_authenticated_sender_login_mismatch

submissions — имя службы порта 465 (getent services submissions). Если имя не разрешается, укажите 465. Строки -o начинаются с пробела — это продолжение определения службы.

reject_authenticated_sender_login_mismatch проверяет адрес MAIL FROM, а не заголовок From:. AUTH PLAIN/LOGIN допустимы только потому, что TLS здесь обязателен.

10. Dovecot 2.4: IMAPS, SASL и LMTP

Для новой установки используем один самостоятельный /etc/dovecot/dovecot.conf без подключения старого conf.d. Так не будет одновременной SQL- и системной аутентификации, дублей listener-ов и параметров 2.3.

Блок подключения к базе отличается для двух СУБД:

PostgreSQL

текст
sql_driver = pgsql
pgsql 127.0.0.1 {
  parameters {
    user = mailreader
    password = MAIL_DB_PASSWORD
    dbname = postfixadmin
  }
}

MariaDB

текст
sql_driver = mysql
mysql 127.0.0.1 {
  user = mailreader
  password = MAIL_DB_PASSWORD
  dbname = postfixadmin
}

Остальной файл общий. Вставьте выбранный блок на место комментария:

/etc/dovecot/dovecot.confтекст
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0

protocols = imap lmtp
listen = *

ssl = required
ssl_min_protocol = TLSv1.2
ssl_server_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_server_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem

auth_allow_cleartext = no
auth_mechanisms = plain login
auth_username_format = %{user | lower}

mail_uid = vmail
mail_gid = vmail
first_valid_uid = 5000
mail_driver = maildir
mail_path = %{home}/Maildir
mailbox_list_index = yes

namespace inbox {
  inbox = yes
  separator = /
  mailbox Sent {
    auto = subscribe
    special_use = \Sent
  }
  mailbox Drafts {
    auto = subscribe
    special_use = \Drafts
  }
  mailbox Trash {
    auto = subscribe
    special_use = \Trash
  }
  mailbox Junk {
    auto = subscribe
    special_use = \Junk
  }
}

# ← сюда блок sql_driver + pgsql/mysql из вкладки выше

passdb sql {
  default_password_scheme = ARGON2ID
  query = SELECT m.username AS user, m.password FROM mailbox m JOIN domain d ON d.domain=m.domain WHERE m.username='%{user}' AND m.active='1' AND d.active='1' AND d.backupmx='0'
}

userdb sql {
  query = SELECT CONCAT('/var/vmail/',m.maildir) AS home, CONCAT(m.quota,'B') AS quota_storage_size FROM mailbox m JOIN domain d ON d.domain=m.domain WHERE m.username='%{user}' AND m.active='1' AND d.active='1' AND d.backupmx='0'
  iterate_query = SELECT m.username AS user FROM mailbox m JOIN domain d ON d.domain=m.domain WHERE m.active='1' AND d.active='1' AND d.backupmx='0'
}

service imap-login {
  inet_listener imap {
    port = 0
  }
  inet_listener imaps {
    port = 993
    ssl = yes
  }
}

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

protocol lmtp {
  postmaster_address = postmaster@example.com
}

mail_plugins {
  quota = yes
}

protocol imap {
  mail_plugins {
    imap_quota = yes
  }
}

quota user {
  driver = count
}

dovecot_config_version должна идти первой. Значение 2.4.0 задаёт базовую версию синтаксиса, а не точную версию пакета. Блоки SQL и переменные соответствуют документации 2.4.[1:1][3] Пути сертификатов в 2.4 указываются без старого префикса <. Сокеты лежат внутри chroot Postfix, поэтому Postfix видит их как private/auth и private/dovecot-lmtp.

bash
chown root:root /etc/dovecot/dovecot.conf
chmod 0600 /etc/dovecot/dovecot.conf
doveconf -n >/dev/null && echo OK

Полный вывод doveconf -n и debug-логи не публикуйте: в них пароль БД.

Квоты

Поле mailbox.quota хранит байты. SQL возвращает их с суффиксом B, ноль — без ограничения. Для новых установок рекомендован драйвер count.[4][5]

bash
doveadm user alice@example.com
doveadm quota get -u alice@example.com

Смените лимит тестового ящика и убедитесь, что Dovecot видит новое значение и ограничивает доставку. Фактический объём ящика PostfixAdmin с драйвером count сам не показывает.

POP3 — только при необходимости

IMAP обычно удобнее: папки и состояние синхронизируются между устройствами. Если POP3S всё же нужен:

bash
apt install dovecot-pop3d

Замените строку на protocols = imap pop3 lmtp и добавьте:

текст
service pop3-login {
  inet_listener pop3 {
    port = 0
  }
  inet_listener pop3s {
    port = 995
    ssl = yes
  }
}

Открывайте только 995. Незашифрованные 110 и 143 не нужны. Проверять вход через telnet с настоящим паролем, как в старой версии статьи, нельзя.

11. Rspamd: антиспам и DKIM

Rspamd и проверяет входящие письма, и подписывает исходящие. Отдельный OpenDKIM не нужен.

bash
apt install rspamd redis-server
systemctl enable --now redis-server rspamd

/etc/rspamd/local.d/redis.conf:

текст
servers = "127.0.0.1";

/etc/rspamd/local.d/worker-proxy.inc:

текст
bind_socket = "127.0.0.1:11332";
milter = yes;
timeout = 120s;
upstream "local" {
  default = yes;
  self_scan = yes;
}

/etc/rspamd/local.d/worker-controller.inc:

текст
bind_socket = "127.0.0.1:11334";

Порты Rspamd и Redis в Интернет не открывайте. Веб-интерфейс Rspamd — через SSH-туннель и с паролем контроллера (хеш даёт rspamadm pw).

Ключ DKIM

bash
install -d -o _rspamd -g _rspamd -m 0700 /var/lib/rspamd/dkim
umask 077
rspamadm dkim_keygen -b 2048 -s mail -d example.com \
  -k /var/lib/rspamd/dkim/example.com.mail.key \
  > /root/example.com.mail.dns.txt
chown _rspamd:_rspamd /var/lib/rspamd/dkim/example.com.mail.key
chmod 0600 /var/lib/rspamd/dkim/example.com.mail.key
cat /root/example.com.mail.dns.txt

Опубликуйте выведенную TXT-запись для mail._domainkey.example.com. Если DNS-панель ждёт только значение, перенесите v=DKIM1; k=rsa; p=… целиком; несколько строк в кавычках — части одного значения. Закрытый .key не публикуется никогда.

/etc/rspamd/local.d/dkim_signing.conf:

текст
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector = "mail";
use_domain = "header";
use_esld = false;
sign_authenticated = true;
sign_local = true;
allow_username_mismatch = false;
allow_hdrfrom_mismatch = false;

Для каждого отправляющего домена нужен свой ключ и своя TXT-запись.[6]

bash
rspamadm configtest
systemctl restart rspamd
ss -lntp | grep -E ':11332|:11334|:6379'
dig +short TXT mail._domainkey.example.com

Только после успешного запуска подключайте milter:

bash
postconf -e 'smtpd_milters = inet:127.0.0.1:11332'
postconf -e 'non_smtpd_milters = $smtpd_milters'
postconf -e 'milter_protocol = 6'
postconf -e 'milter_default_action = tempfail'
postconf -e 'milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}'

tempfail выбран сознательно: если Rspamd недоступен, Postfix временно откажет, и нормальный отправитель повторит попытку. Это требует мониторинга Rspamd. Вариант accept доступнее, но во время сбоя пропускает письма без проверки и без подписи.[7]

Не отклоняйте письма только по SPF: пересылка ломает SPF, а DKIM при этом остаётся верным. DNSBL-списки часто не отвечают публичным резолверам — при необходимости поднимите свой рекурсивный резолвер. Заголовок спама сам по себе не переносит письмо в «Спам»: для этого нужен Sieve (он есть в установщике Mailstack). Антивирус в ручную схему не входит.

12. Первый запуск и firewall

bash
postfix check
doveconf -n >/dev/null
rspamadm configtest
apache2ctl configtest
systemctl enable --now dovecot postfix
systemctl restart dovecot postfix
systemctl --no-pager status dovecot postfix rspamd
ss -lntp | grep -E ':(25|465|587|993)\b'
ls -l /var/spool/postfix/private/auth /var/spool/postfix/private/dovecot-lmtp

После изменения inet_protocols нужен restart Postfix, дальше достаточно reload.

На выделенном сервере без своего firewall подойдёт UFW. Сначала разрешите фактический порт SSH и убедитесь, что есть аварийная консоль:

bash
apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 25/tcp
ufw allow 465/tcp
ufw allow 587/tcp
ufw allow 993/tcp
ufw allow 80/tcp
ufw enable
ufw status verbose

80/tcp нужен только для HTTP-01 certbot. Для POP3S добавьте 995. Порты 5432/3306, 6379, 11332–11334 и LMTP наружу не открываются. Не накладывайте UFW вслепую на существующие правила nftables, Docker или firewall провайдера.

13. Проверка работы

TLS

С другого компьютера, по имени из сертификата:

bash
openssl s_client -connect mail.example.com:993 \
  -servername mail.example.com -verify_hostname mail.example.com \
  -verify_return_error -CApath /etc/ssl/certs

openssl s_client -starttls smtp -connect mail.example.com:587 \
  -servername mail.example.com -verify_hostname mail.example.com \
  -verify_return_error -CApath /etc/ssl/certs

openssl s_client -connect mail.example.com:465 \
  -servername mail.example.com -verify_hostname mail.example.com \
  -verify_return_error -CApath /etc/ssl/certs

Совпасть должны имя, цепочка доверия и срок действия. Один баннер SMTP/IMAP доверенного TLS не доказывает.

Аутентификация

bash
doveadm user alice@example.com
doveadm auth test alice@example.com

Пароль вводите в запросе, не в аргументах. Проверьте верный пароль, неверный, несуществующий адрес и выключенный ящик.

Отправка и доставка

С внешнего компьютера, на свой реальный контрольный адрес:

bash
swaks --server mail.example.com --port 587 --tls \
  --auth LOGIN --auth-user alice@example.com --auth-password \
  --from alice@example.com --to external-recipient@example.net

--auth-password без значения запросит пароль интерактивно. Для 465 используйте --tls-on-connect. Проверка должна подтвердить:

  1. TLS включается до AUTH.
  2. Верный пароль проходит, неверный — нет.
  3. Без AUTH отправка на 587/465 отвергается, даже на свой домен.
  4. Вошедший как alice@example.com не может указать MAIL FROM: bob@example.com.
  5. Письмо пришло внешнему получателю, а в его заголовке Authentication-Results (добавленном принимающей стороной) — spf=pass, dkim=pass, dmarc=pass.
  6. Ответное письмо извне доставляется в IMAP-ящик.

Приём на порт 25 (отправитель — ваш реальный тестовый адрес):

bash
swaks --server mail.example.com --port 25 \
  --from sender@example.net --to alice@example.com

250 … queued as … — это принятие в очередь, а не доставка.

Проверка на open relay

Только с внешнего узла и без авторизации: с 127.0.0.1 тест бессмыслен, loopback входит в mynetworks.

bash
swaks --server mail.example.com --port 25 \
  --from sender@external.example --to recipient@another-external.example \
  --quit-after RCPT

Подставьте два настоящих чужих домена. На RCPT TO должен быть отказ в relay. Если сервер принял — в эксплуатацию его не вводить. Так же проверьте неизвестного получателя в своём домене: отказ должен прийти во время SMTP-сессии, а не отдельным bounce (иначе сервер рассылает backscatter).

Настройки почтового клиента

Параметр Значение
Входящий сервер mail.example.com, IMAP, порт 993, SSL/TLS
Исходящий сервер mail.example.com, 587 STARTTLS или 465 SSL/TLS
Аутентификация SMTP обязательна
Логин полный адрес, например alice@example.com

14. Логи, очередь и типичные ошибки

Если /var/log/mail.log не появился, добавьте правило mail.* -/var/log/mail.log в отдельный файл в /etc/rsyslog.d/, проверьте rsyslogd -N1 и перезапустите rsyslog. Второе такое правило не создавайте.

bash
tail -f /var/log/mail.log
journalctl -u dovecot -u rspamd --since '30 minutes ago'
journalctl -u 'postfix@-.service' --since '30 minutes ago'
postqueue -p
postcat -q QUEUE_ID
postqueue -i QUEUE_ID

В Debian журнал Postfix пишет instance-unit postfix@-.service. postcat показывает содержимое письма — не публикуйте его без обезличивания. Не удаляйте всю очередь и не гоняйте flush по кругу вместо поиска причины.

Симптом Что проверять
Dovecot не стартует: неизвестные настройки остатки конфигурации 2.3, %u/%d, нет version-параметров, не установлен dovecot-pgsql/dovecot-mysql
User unknown запись в mailbox, активность ящика и домена, полный логин, SQL-карты
Relay access denied при авторизованной отправке порт, TLS, AUTH, сокет private/auth, ограничения в master.cf
Sender address rejected: not owned by user MAIL FROM не принадлежит вошедшему по sender-login.cf
Нет соединения с LMTP Dovecot запущен, lmtp в protocols, сокет существует, путь и прÐ