Полностью переработанная версия статьи о 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
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-запись должна возвращать тот же адрес.
Имя и время
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.
dig +short MX example.com
dig +short A mail.example.com
dig +short -x 203.0.113.10
dig +short TXT example.com2. Установка пакетов
Выберите СУБД. Набор пакетов отличается только драйверами базы:
PostgreSQL
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 rsyslogMariaDB
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.
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: сервисы с промежуточной конфигурацией не должны быть доступны из Интернета.
Резервная копия пакетной конфигурации
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 свободны:
getent passwd 5000; getent group 5000
getent passwd vmail; getent group vmailЕсли заняты — выберите другие значения и замените их во всех настройках ниже.
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
# Debian по умолчанию слушает только localhost — проверьте:
runuser -u postgres -- psql -Atc 'SHOW listen_addresses'
ss -lntp | grep ':5432'
# SQL-консоль без истории
runuser -u postgres -- env PSQL_HISTORY=/dev/null psqlMariaDB
# В /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 mariadbPostgreSQL
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;
\qMariaDB
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:
<?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 и запуск от имени веб-сервера:
doveadm -O pw -l
runuser -u www-data -- /usr/bin/doveadm -O pw -s ARGON2IDПароль вводится интерактивно; полученный хеш никуда не публикуйте. Если ARGON2ID недоступен, выберите другую стойкую схему (например, SHA512-CRYPT) одновременно в PostfixAdmin и Dovecot. К MD5 не возвращайтесь.
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 на:
Listen 127.0.0.1:80Если Apache уже обслуживает сайты, глобальные настройки не трогайте: сделайте отдельный virtual host с HTTPS и доступом только из VPN или с административных IP.
a2disconf postfixadmin
a2dissite 000-default/etc/apache2/sites-available/mailadmin.conf:
<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>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:
ssh -N -L 8080:127.0.0.1:80 admin@mail.example.comHTTP здесь допустим, потому что трафик идёт внутри SSH. Дальше:
- На странице настройки сформируйте хеш setup-пароля и подставьте его вместо
REPLACE_WITH_SETUP_HASH. - Создайте схему БД и администратора.
- Создайте домен
example.com, ящикalice@example.comи алиасыpostmaster@,abuse@,dmarc@на существующий ящик. - Закройте setup внутри virtual host и перезагрузите Apache:
<LocationMatch "^/(setup|upgrade)\.php$">
Require all denied
</LocationMatch>Для планового обновления схемы открывайте setup временно и только через административный канал. Наружу публикуется только public, а не весь /usr/share/postfixadmin.
Только для PostgreSQL: когда таблицы созданы, выдайте читателю права. ALTER DEFAULT PRIVILEGES нужен, чтобы таблицы, добавленные будущими обновлениями PostfixAdmin, тоже были доступны:
-- 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;Проверьте данные:
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, поэтому на время выпуска его останавливаем:
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:
#!/bin/sh
set -eu
systemctl stop apache2/etc/letsencrypt/renewal-hooks/post/10-start-mailadmin:
#!/bin/sh
set -eu
systemctl start apache2/etc/letsencrypt/renewal-hooks/deploy/20-reload-mail (создайте после настройки Postfix и Dovecot):
#!/bin/sh
set -eu
postfix check
systemctl reload postfix
doveconf -n >/dev/null
systemctl reload dovecotПри DNS-01 или webroot останавливать Apache не нужно, pre/post-хуки тогда не создавайте.
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
MAP=pgsqlMariaDB
MAP=mysqlpostconf -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.comnewaliases8. SQL-карты Postfix
install -d -o root -g postfix -m 0750 /etc/postfix/sqlКаждый из пяти файлов начинается с общего блока (пароль замените):
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 — основной домен и активные домены-алиасы:
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, путь здесь не нужен):
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:
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 через них не поддерживаются намеренно:
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 — кто может отправлять с этого адреса: владелец ящика, а для алиаса — его активные получатели-ящики:
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'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 уже есть, отредактируйте их, а не добавляйте дубли:
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_mismatchsubmissions — имя службы порта 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
}Остальной файл общий. Вставьте выбранный блок на место комментария:
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.
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]
doveadm user alice@example.com
doveadm quota get -u alice@example.comСмените лимит тестового ящика и убедитесь, что Dovecot видит новое значение и ограничивает доставку. Фактический объём ящика PostfixAdmin с драйвером count сам не показывает.
POP3 — только при необходимости
IMAP обычно удобнее: папки и состояние синхронизируются между устройствами. Если POP3S всё же нужен:
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 не нужен.
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
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]
rspamadm configtest
systemctl restart rspamd
ss -lntp | grep -E ':11332|:11334|:6379'
dig +short TXT mail._domainkey.example.comТолько после успешного запуска подключайте milter:
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
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 и убедитесь, что есть аварийная консоль:
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 verbose80/tcp нужен только для HTTP-01 certbot. Для POP3S добавьте 995. Порты 5432/3306, 6379, 11332–11334 и LMTP наружу не открываются. Не накладывайте UFW вслепую на существующие правила nftables, Docker или firewall провайдера.
13. Проверка работы
TLS
С другого компьютера, по имени из сертификата:
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 не доказывает.
Аутентификация
doveadm user alice@example.com
doveadm auth test alice@example.comПароль вводите в запросе, не в аргументах. Проверьте верный пароль, неверный, несуществующий адрес и выключенный ящик.
Отправка и доставка
С внешнего компьютера, на свой реальный контрольный адрес:
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. Проверка должна подтвердить:
- TLS включается до AUTH.
- Верный пароль проходит, неверный — нет.
- Без AUTH отправка на 587/465 отвергается, даже на свой домен.
- Вошедший как
alice@example.comне может указатьMAIL FROM: bob@example.com. - Письмо пришло внешнему получателю, а в его заголовке
Authentication-Results(добавленном принимающей стороной) —spf=pass,dkim=pass,dmarc=pass. - Ответное письмо извне доставляется в IMAP-ящик.
Приём на порт 25 (отправитель — ваш реальный тестовый адрес):
swaks --server mail.example.com --port 25 \
--from sender@example.net --to alice@example.com250 … queued as … — это принятие в очередь, а не доставка.
Проверка на open relay
Только с внешнего узла и без авторизации: с 127.0.0.1 тест бессмыслен, loopback входит в mynetworks.
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. Второе такое правило не создавайте.
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, сокет существует, путь и прÐ |