Сучасне TLS 1.3 у Postfix і Dovecot: cipher suites, протокольні floors і оцінка A+
Більшість hardening-гайдів помиляються на рівні фундаменту: жорсткий cipherlist фізично не керує TLS 1.3. Розбираємо дві поверхні шифрів в OpenSSL, попортові політики TLS, DANE/MTA-STS і те, як реально отримати A+ у поштових тестах.
EvilMail Team16 липня 2026 р.14 хв читання
Типовий сценарій. Адмін бере cipherlist з популярного гайда, вставляє його в main.cf, перезапускає Postfix. testssl.sh показує зелений A, всі задоволені. Потім у логах з'являється рядок:
TLS connection established from mx.sender.example:
TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
Шифр TLS 1.3 — а в конфізі його немає жодного. Список, який мав «жорстко контролювати шифри», його не контролює. І це не баг, а фундамент, який більшість гайдів мовчки пропускає.
Тут дві поверхні непорозуміння, які ми розберемо до кінця. Перша: OpenSSL керує шифрами TLS 1.2 і TLS 1.3 через два фізично різні API, і smtpd_tls_exclude_ciphers не має жодного впливу на TLS 1.3. Друга суто SMTP-специфічна: порт 25 і порти 587/465 вимагають протилежних політик, і надто агресивний floor на 25-му не підвищує безпеку, а виштовхує лист у plaintext.
Чому «жорсткий» cipherlist не вмикає TLS 1.3
TLS 1.3 у Postfix і Dovecot: cipher suites та оцінка A+ | evilmail.pro — EvilMail Blog
Починаючи з OpenSSL 1.1.1, керування шифрами розщеплено надвоє. Класичний cipherlist (SSL_CTX_set_cipher_list) описує набір для TLS 1.2 і нижче. Для TLS 1.3 з'явився окремий параметр — ciphersuites (SSL_CTX_set_ciphersuites), і він живе повністю окремо. Це не одна ручка з двома режимами, а дві незалежні поверхні.
Наслідки конкретні:
smtpd_tls_exclude_ciphers та tls_high_cipherlist діють лише на нижню гілку. Ви можете вписати туди !RC4:!3DES:!aNULL — на TLS 1.3 це ніяк не вплине, бо там інший API.
tls_preempt_cipherlist = yes (перевага серверного порядку) на TLS 1.3 теж не працює так, як ви очікуєте: набір TLS 1.3 фіксований, і хоча вибір робить сервер, він робить його з жорстко заданих трьох шифрів, а не з вашого впорядкованого cipherlist.
І це нормально, бо у TLS 1.3 просто немає «поганих» шифрів. Стандарт залишив рівно три AEAD-набори, і всі три безпечні:
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256
Жодного RC4, жодного CBC, жодного RSA key exchange без forward secrecy. Уся стара отрута лишилася в TLS 1.2. Тому «hardening» TLS 1.3 — це не викидання шифрів, а свідоме керування тим самим набором. У Postfix за нього відповідає tls_tls13_ciphers, у Dovecot — ssl_cipher_suites. Якщо ви ці параметри не чіпали — ви керуєте лише половиною свого TLS-стека.
Порт 25 проти 587/465: одна конфігурація не підходить
Друга помилка коштує дорожче за першу, бо вона впливає на доставлюваність. У SMTP TLS буває двох принципово різних сортів.
На порту 25 (MTA↔MTA) працює opportunistic TLS: smtpd_tls_security_level = may. Відправник спробує STARTTLS, і якщо не вийде — відправить у відкритому вигляді. Це навмисна поведінка стандарту: краще доставити лист незашифрованим, ніж втратити його. Порт 25 не автентифікує клієнта і не може вимагати TLS без ризику мовчазно поламати пошту від легітимних, але старих відправників.
На портах 587 (submission, STARTTLS) і 465 (submission over implicit TLS) працює mandatory TLS: smtpd_tls_security_level = encrypt. Тут клієнт — це ваш власний користувач із логіном і паролем, він зобов'язаний шифруватися, і smtpd_tls_auth_only = yes забороняє AUTH до підняття TLS.
Пастка plaintext-fallback виглядає так. Хтось читає гайд «зробіть TLS 1.3 обов'язковим скрізь» і виставляє на порту 25 smtpd_tls_mandatory_protocols = >=TLSv1.3. Приходить MTA, який уміє тільки TLS 1.2. Handshake не складається — і замість «жорсткого» TLS відправник відкочується у plaintext, бо на 25-му рівень усе одно may. Ви одночасно втратили і шифрування, і не помітили цього, поки не грепнули логи.
Тому floor треба задавати попортово, у master.cf, а не глобально. У 2026-му розумний floor для порту 25 — це TLS 1.2: частка MTA в інтернеті, які вміють лише TLS 1.0/1.1, вже мізерна, і відсіювати їх безпечно. А от floor TLS 1.3 на 25-му ще зарано — забагато легітимних серверів на 1.2. На 587/465, де клієнти ваші й контрольовані, floor TLS 1.2 обов'язковий, а TLS 1.3 можна вмикати сміливо.
Postfix: main.cf і master.cf
Ось базовий блок для main.cf. Кожен параметр робить рівно одне.
ini
# Рівень для порту 25 — opportunistic, не ламаємо чужу пошту
smtpd_tls_security_level = may
# Протокольні floors: TLS 1.2 як мінімум, старе вимкнено явно
smtpd_tls_protocols = >=TLSv1.2, !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = >=TLSv1.2
# Клас шифрів для TLS ≤ 1.2
smtpd_tls_ciphers = high
smtpd_tls_mandatory_ciphers = high
smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5,
PSK, SRP, 3DES, SEED, IDEA, CAMELLIA
# Cipherlist для TLS ≤ 1.2 (усі з PFS)
tls_high_cipherlist = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM
# ОКРЕМА поверхня — набір TLS 1.3
tls_tls13_ciphers = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
# Перевага серверного порядку (діє на ≤1.2)
tls_preempt_cipherlist = yes
# Криві: X25519 першою
smtpd_tls_eecdh_grade = auto
tls_eecdh_auto_curves = X25519:X448:prime256v1:secp384r1
Ключове тут — рядок tls_tls13_ciphers. Без нього ви не керуєте TLS 1.3; з ним ви явно фіксуєте порядок переваги трьох дозволених наборів. smtpd_tls_exclude_ciphers навмисно містить CAMELLIA і 3DES — це TLS 1.2 сміття, яке інакше пролізе через high.
Тепер master.cf — саме тут вмикається mandatory-режим для клієнтських портів:
ini
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_mandatory_protocols=>=TLSv1.2
-o smtpd_tls_auth_only=yes
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_auth_only=yes
0-RTT/early-data ми свідомо не вмикаємо — для SMTP і IMAP це ризик replay-атаки, а виграшу в латентності на пошті нема. Postfix і Dovecot за замовчуванням його й не пропонують, тож просто не чіпайте.
Тут ssl_cipher_list — це TLS 1.2, а ssl_cipher_suites — TLS 1.3, тобто та сама пара поверхонь, що й у Postfix, лише під іншими іменами (ssl_cipher_suites доступний з Dovecot 2.3.15+). ssl = required означає, що на плейн-портах 143 (IMAP) і 110 (POP3) STARTTLS обов'язковий, а без нього логін заборонено. Порти 993 (IMAPS) і 995 (POP3S) — implicit TLS, шифрування з першого байта. STARTTLS на плейн-портах лишаємо required, а не викидаємо самі порти: багато клієнтів досі стукають у 143 і коректно апгрейдяться.
Криві, DH і forward secrecy
X25519 має бути першою кривою в обох демонах — вона швидка, константного часу і без сумнівних параметрів NIST. У TLS 1.3 forward secrecy вбудований у стандарт: усі три шифри ефемерні за визначенням, окремо думати про PFS там не треба.
DHE лишається тільки для сумісності з TLS 1.2. І тут важливе оновлення практики: не генеруйте саморобний `dhparam.pem` руками. Named-групи FFDHE з RFC 7919 (ffdhe2048, ffdhe4096) перевірені й безпечніші за випадковий 2048-бітний параметр із вашого сервера. Якщо ваш стек ще вимагає файл, згенеруйте його разово:
bash
openssl dhparam -out /etc/dovecot/dh.pem 4096
У чистому TLS 1.3-only середовищі цей файл узагалі не використовується — він потрібен лише як запасний варіант для 1.2-клієнтів.
DANE, MTA-STS і TLS-RPT — що реально дає A+
Ось де ховається різниця між A і A+ у поштових тестах. Це не екзотичні шифри, а DNS-записи, що доводять: ваш TLS не можна знизити атакою «людина посередині».
DANE (TLSA). Прив'язує сертифікат до DNS через DNSSEC. Публікуєте хеш публічного ключа для порту 25:
Отриманий хеш іде в запис 3 1 1 (DANE-EE, SubjectPublicKeyInfo, SHA-256):
_25._tcp.mail.evilmail.pro. IN TLSA 3 1 1 <sha256-hash>
DANE потребує DNSSEC на зоні — без підпису запис нічого не гарантує. І пам'ятайте про ротацію ключів: 3 1 1 прив'язаний до конкретного публічного ключа, а Let's Encrypt за замовчуванням генерує новий ключ на кожен перевипуск. Тож або фіксуйте ключ (certbot --reuse-key), або публікуйте 3 1 1 разом із запасним 2 1 1 на проміжний CA і оновлюйте TLSA до, а не після ротації — інакше DANE поламає вашу вхідну пошту.
MTA-STS. Той самий захист від downgrade, але без DNSSEC. Потрібен TXT-запис плюс policy-файл по HTTPS:
_mta-sts.evilmail.pro. IN TXT "v=STSv1; id=20260704T000000"
Файл на https://mta-sts.evilmail.pro/.well-known/mta-sts.txt:
І головне про грейдинг: SSL Labs оцінює тільки HTTPS, не SMTP — не шукайте там свою пошту. Для пошти еталон — internet.nl/mail з ціллю 100%, а також Hardenize, CryptCheck (cryptcheck.fr) і checktls.com. Практичне визначення A+ для пошти: TLS 1.3 присутній, немає слабких шифрів і протоколів, PFS скрізь, сильні криві, опубліковані DANE і MTA-STS.
Що зазвичай зрізає оцінку: живий TLS 1.0 на якомусь порту, слабка крива в ssl_curve_list, відсутній DANE, або plaintext-fallback, який видно тільки в логах. Тому останній рубіж — grep по журналу Postfix на Anonymous TLS, Untrusted TLS і verify=NONE: ці рядки означають TLS без сертифіката, з невалідованим сертифікатом або без перевірки вихідного з'єднання — тобто частину трафіку, що йде повз ваш «жорсткий» TLS.
Чеклист перед продакшеном
Floors виставлені попортово в master.cf, а не глобально: 25 — TLS 1.2 opportunistic, 587/465 — TLS 1.2 mandatory.
tls_tls13_ciphers (Postfix) і ssl_cipher_suites (Dovecot) явно задані — інакше половина стека некерована.
smtpd_tls_exclude_ciphers містить 3DES, RC4, CAMELLIA, aNULL, eNULL.
X25519 стоїть першою в tls_eecdh_auto_curves і ssl_curve_list.
DH — named FFDHE (RFC 7919), а не саморобний dhparam.
0-RTT / early-data вимкнено (за замовчуванням і є).
Опубліковані DANE (3 1 1), MTA-STS (enforce) і TLS-RPT; для TLSA увімкнено DNSSEC, а ротація ключів узгоджена з оновленням запису.
Прогнано testssl.sh --starttls smtp та internet.nl/mail — ціль 100%.
Налаштований моніторинг логів Postfix на Anonymous TLS, Untrusted TLS і verify=NONE.
Правильний TLS на пошті — це не колекція рідкісних шифрів, а розуміння, яка ручка що вмикає і на якому порту. Дві поверхні OpenSSL, попортові floors, чесний DANE/MTA-STS — і A+ приходить сам, без магії.