Порт 25 не для людей: где утекает пароль
На порт 25 стучатся чужие MTA, чтобы отдать вам почту. Живой пользователь с логином и паролем там не появляется никогда. Тем не менее на тысячах инсталляций Postfix smtpd_sasl_auth_enable включён глобально в main.cf, а значит SASL-механизмы предлагаются на всех сервисах разом — включая 25/tcp, где TLS всего лишь оппортунистический. Сервер честно отдаёт 250-AUTH PLAIN LOGIN в ответ на EHLO до всякого шифрования. Клиент, настроенный чуть криво (или библиотека, которая «пробует 25-й, раз он открыт»), отправляет AUTH PLAIN в base64 по голому TCP. Пароль снимается пассивным сниффером в кафе одной командой tcpdump.
Проблема не в самом AUTH, а в том, что его предлагают там, где нет гарантированного канала. Правильная модель — три физически разных интерфейса с разными правилами, а не один «универсальный» smtpd, который пытается обслужить и MTA-релей, и почтовый клиент. Разнесём двери, сделаем AUTH невозможным без установленного TLS, а потом закроем второй фронт: украденную учётку, из-под которой уходит спам.
Три двери: 25, 587 и 465 — и почему их нельзя путать
Разница между портами не косметическая — она определяет весь набор правил:
- 25/tcp — relay между почтовыми серверами. TLS оппортунистический (
may), потому что заставить весь интернет шифроваться нельзя: иначе половина легитимной почты отвалится. AUTH здесь не нужен вообще — чужие MTA не аутентифицируются, они просто доставляют письмо вашему домену. Единственная защита от опен-релея —reject_unauth_destination. - 587/tcp submission — дверь для ваших клиентов. STARTTLS обязателен, AUTH обязателен, без валидного
sasl_usernameписьмо никуда не уходит. - 465/tcp submissions — implicit TLS. Соединение шифруется на уровне сокета, до первой SMTP-команды. RFC 8314 (2018) реабилитировал этот порт после многолетнего статуса «deprecated» и прямо рекомендует его как основной для почтовых клиентов: implicit TLS не оставляет окна для STARTTLS-stripping атаки.
AUTH только поверх TLS: smtpd_tls_auth_only
Ключевой параметр — smtpd_tls_auth_only = yes. С ним Postfix физически не включает 250-AUTH в ответ на EHLO, пока клиент не выполнит STARTTLS. Механизмы SASL просто отсутствуют в списке возможностей до установки TLS-канала. Даже если где-то по недосмотру smtpd_sasl_auth_enable=yes протёк на 25-й порт, снять пароль в открытом виде уже не выйдет — предлагать нечего.
Держим глобальный дефолт в main.cf строгим, а AUTH включаем точечно через -o в master.cf:
# main.cf — глобальные дефолты
smtpd_sasl_auth_enable = no
smtpd_tls_auth_only = yes
smtpd_tls_security_level = may # порт 25: оппортунистический TLS
# запрет древних протоколов на обоих направлениях
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_ciphers = high
tls_preempt_cipherlist = yesTLSv1.2 — практический минимум в 2026, TLSv1.3 предпочтителен. tls_preempt_cipherlist = yes заставляет сервер навязывать свой порядок шифров, а не доверять выбору клиента.
Сами клиентские сервисы описываем в master.cf. Submission на 587 со STARTTLS:
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_auth_only=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_recipient_restrictions=permit_sasl_authenticated,rejectИ implicit-TLS вариант на 465 (RFC 8314), с wrappermode:
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_client_restrictions=permit_sasl_authenticated,rejectОбратите внимание: smtpd_tls_security_level=encrypt на клиентских портах — это не may. Клиент без TLS туда просто не зайдёт. На 25-м оставляем may, иначе поломаем входящую почту от серверов без TLS.
Первая линия — нативные лимиты Postfix (anvil)
Демон anvil встроен в Postfix и собирает статистику по сессиям. Он даёт грубые, но бесплатные лимиты, которых хватает против тупого брутфорса и лавины соединений. Вешаем их на submission-инстанс:
smtpd_client_connection_rate_limit = 30 # новых коннектов с одного IP за интервал
smtpd_client_message_rate_limit = 100 # сообщений с одного IP за интервал
smtpd_client_connection_count_limit = 20 # одновременных сессий с одного IP
smtpd_client_recipient_rate_limit = 200 # получателей с одного IP за интервал
anvil_rate_time_unit = 60sКритично понимать ограничение: anvil считает по `client_address`, а не по `sasl_username`. За одним корпоративным NAT сидят сотни легитимных пользователей с общим внешним IP — в лимит они упрутся все вместе. А злоумышленник с ботнета обходит anvil, размазав отправку по тысяче адресов. Поэтому anvil — защита от шума на сетевом уровне, но не инструмент квотирования по учётке. Per-user квота требует policy-демона.
policyd / postfwd: политики отправки и суточные квоты
Здесь нужна честность. Классический policyd v2 (cluebringer) — тот самый, что многие до сих пор гуглят — не обновлялся примерно с 2015 года. MySQL-схема, PHP-морда и Perl-демон сгнили, на современном стеке он собирается с бубном и течёт. В 2026 практический стандарт per-user rate limiting в Postfix — это postfwd (postfix firewall daemon): один Perl-файл, конфиг без базы данных, живой и поддерживаемый.
Работает через policy delegation: в списке ограничений submission-инстанса Postfix встречает check_policy_service, открывает TCP-сокет к демону, отдаёт атрибуты запроса и ждёт вердикт. Критичен порядок: check_policy_service должен стоять перед permit_sasl_authenticated. Postfix обходит список ограничений до первого вердикта permit/reject; если permit_sasl_authenticated отработает раньше, обход прекратится — и до политики дело не дойдёт. Именно тот аутентифицированный отправитель, которого мы хотим квотировать, проскочит без проверки. Поэтому в submission-инстансе master.cf меняем строку recipient_restrictions:
# master.cf, submission — политика ПЕРЕД permit_sasl_authenticated
-o smtpd_recipient_restrictions=check_policy_service inet:127.0.0.1:10040,permit_sasl_authenticated,reject
# main.cf — сколько ждать ответ демона, прежде чем отпустить сессию
policy_time_limit = 3600sasl_username попадёт в запрос независимо от позиции в списке: RCPT идёт уже после AUTH, и Postfix передаёт демону все известные на этот момент атрибуты. Позиция важна только из-за короткого замыкания на permit/reject.
Конфиг postfwd с суточной квотой на письма и получателей:
# /etc/postfwd/postfwd.cf
id=SUBMIT-QUOTA
sasl_username=~^.+$
action=rate(sasl_username/500/86400/450 4.7.1 daily message quota exceeded)
id=RCPT-BURST
sasl_username=~^.+$
action=rcpt(sasl_username/1000/86400/450 4.7.1 recipient quota exceeded)rate(...) считает сообщения, rcpt(...) — получателей, ключ группировки — sasl_username, окно 86400 секунд (сутки). При превышении postfwd возвращает 450 — временный отказ, клиент повторит позже, а не потеряет письмо. Запуск демона:
postfwd -f /etc/postfwd/postfwd.cf \
--interface=127.0.0.1 --port=10040 \
--summary=600 --daemonФлаг --summary=600 каждые 10 минут пишет в лог статистику срабатываний правил — удобно видеть, кто упёрся в квоту.
Как Postfix разговаривает с policy-демоном
Протокол делегирования — plain text: Postfix шлёт набор строк атрибут=значение, блок терминируется пустой строкой, демон отвечает одной строкой action=... и снова пустой строкой. Полезные атрибуты запроса:
request=smtpd_access_policy,protocol_state(RCPT / DATA / END-OF-MESSAGE)sasl_username,sasl_method,client_address,sender,recipient,recipient_count,sizeencryption_protocol,encryption_cipher— можно строить правила «требую TLSv1.3 для этого класса отправителей»
Возможные ответы демона: DUNNO (не моё решение, идём дальше по цепочке), OK, DEFER/DEFER_IF_PERMIT (временный отказ), REJECT, HOLD (в карантин), DISCARD (тихо проглотить). Обмен можно эмулировать вручную через netcat — отдать блок атрибутов и посмотреть вердикт:
printf 'request=smtpd_access_policy\[email protected]\nrecipient_count=1\n\n' \
| nc 127.0.0.1 10040Компрометация учётки: детект и торможение
Второй сценарий — пароль валидный, но украденный. TLS тут не спасает: злоумышленник аутентифицируется честно. Работают два уровня.
fail2ban ловит брутфорс по SASL. Джейл postfix-sasl смотрит на mail.log и банит IP, который перебирает пароли:
[postfix-sasl]
enabled = true
port = smtp,submission,submissions
filter = postfix-sasl
logpath = /var/log/mail.log
maxretry = 5
findtime = 600
bantime = 3600Фильтр цепляется за строки SASL LOGIN authentication failed. Пять неудач за 10 минут — час бана.
postfwd ловит аномалию уже после успешного логина: резкий всплеск разных получателей — классический признак угнанной учётки, из которой погнали спам-рассылку. Для подозрительного потока правильнее не REJECT, а HOLD: письма уходят в hold-очередь, откуда их можно разобрать руками:
id=ANOMALY-HOLD
sasl_username=~^.+$
action=rcpt(sasl_username/300/3600/HOLD suspicious burst - quarantined)Что лежит в карантине, видно в postqueue -p — held-письма помечены !. Дальше — либо освободить всё (postsuper -H ALL), либо удалить рассылку целиком (postsuper -d ALL hold). Грепнуть логи по факту компрометации:
grep 'sasl_username=' /var/log/mail.log | awk -F'sasl_username=' '{print $2}' | sort | uniq -c | sort -rnЧеклист внедрения и проверка боем
smtpd_sasl_auth_enable = noвmain.cf— AUTH включается только через-oна 587/465smtpd_tls_auth_only = yesглобально — механизмы AUTH скрыты до STARTTLS- порт 25 (
smtp inet) безsmtpd_sasl_auth_enable=yesи сsecurity_level = may - 587 с
security_level=encrypt, 465 сtls_wrappermode=yes - запрещены SSLv2/3, TLSv1.0/1.1;
mandatory_ciphers = high - нативные лимиты anvil выставлены на submission-инстансе
check_policy_service
Проверяем боем. Баннер EHLO не должен содержать AUTH до STARTTLS:
openssl s_client -starttls smtp -connect mail.evilmail.pro:587 -crlf
# в первом EHLO-ответе строки 250-AUTH быть НЕ должноAUTH без TLS не должен предлагаться, с TLS — работать:
# без TLS: AUTH не анонсируется, swaks завершится «no supported auth type»;
# принудительный AUTH получит 530 5.7.0 Must issue a STARTTLS command first
swaks --server mail.evilmail.pro:587 --auth LOGIN --auth-user [email protected]
# с TLS: аутентификация проходит
swaks --server mail.evilmail.pro:587 --auth LOGIN --auth-user [email protected] -tlsСверка конфигов и счётчиков политики:
postconf -Mf # форматированный master.cf, проверить -o
postconf -n | grep -E 'tls_auth_only|sasl_auth'
postfwd --summary # сколько раз сработали правила квотБольшую часть задач по защите submission закрывают встроенные счётчики Postfix и один параметр smtpd_tls_auth_only. Внешний policy-демон нужен ровно там, где anvil бессилен — per-user квоты и детект компрометации по sasl_username. Ставьте postfwd, а не мёртвый cluebringer, и не пускайте AUTH на порт, где нет гарантированного шифра.


