OAuth2 и XOAUTH2 для IMAP и SMTP: настройка Dovecot с внешним провайдером токенов
Пароль в почтовом клиенте — это bearer-секрет, который живёт годами в keychain телефона и утекает при первом фишинге. Разбираем, как принимать короткоживущие OAuth2-токены от Keycloak, Authentik или Azure через SASL XOAUTH2 в своём Dovecot и Postfix: рабочие конфиги, валидация через introspection и локальный JWT, отладка и продакшен-чеклист.
EvilMail Team1 августа 2026 г.14 мин чтения
Почему basic auth в IMAP/SMTP пора закапывать
Пароль в почтовом клиенте — это не пароль в привычном смысле. Это долгоживущий bearer-секрет, который клиент сохраняет один раз и потом молча отправляет на сервер при каждом соединении: телефон синхронизирует IMAP каждые пять минут годами, ноутбук — при каждом пробуждении. Секрет лежит в keychain, в SQLite Thunderbird, в бэкапе iCloud. Он не ротируется, у него нет MFA на уровне протокола, и его можно воспроизводить сколько угодно раз: утёк дамп dovecot-passwd, перехватили сессию на кривом Wi-Fi без TLS, фишинг увёл app-password — и злоумышленник читает ящик ровно столько, сколько сам захочет.
App-password, который так любят выдавать «в обход 2FA», делает только хуже: это тот же вечный пароль, но с иллюзией безопасности. Именно поэтому крупные провайдеры его вычистили. Google поэтапно отключал доступ по паролю для Gmail в 2022–2024 годах, Microsoft отключил basic auth для основных протоколов Exchange Online к 2023 году. Оба теперь требуют OAuth2 через SASL-механизм XOAUTH2. Если вы поднимаете собственную почтовую инфраструктуру в 2026 году, токен-аутентификация — это не мода, а базовая гигиена: вы принимаете короткоживущий access-токен, а не вечный секрет.
Ключевая сложность в том, что IMAP и SMTP — не HTTP. Redirect-флоу OAuth, к которому все привыкли в вебе, здесь невозможен: у почтового протокола нет браузера, нет Location
OAuth2/XOAUTH2 для IMAP и SMTP: настройка Dovecot с внешним IdP — EvilMail Blog
-заголовка, нет callback-URL. Поэтому клиент получает токен
вне
почтового протокола — через отдельный OAuth-обмен с провайдером — а затем предъявляет уже готовый bearer через SASL внутри IMAP/SMTP-сессии. Разделение «где берётся токен» и «где он предъявляется» — центральная идея всей конструкции.
XOAUTH2 и OAUTHBEARER: что реально летит по проводу
Есть два SASL-механизма для bearer-токенов. XOAUTH2 придумал Google, и он стал стандартом де-факто просто потому, что его реализуют все клиенты. OAUTHBEARER описан в RFC 7628 — это «правильный» стандартизованный механизм, но по распространённости он проигрывает.
Формат строки XOAUTH2 максимально прост. Клиент собирает ASCII-строку, где поля разделены байтом 0x01 (Ctrl+A, в документации обозначается ^A), и кодирует её в base64:
Обратите внимание на два 0x01 в конце. Первый закрывает поле auth, второй — пустое поле «дополнительных параметров», обязательное по формату. Декодируется всё обратно тривиально:
OAUTHBEARER технически лучше: он кладёт в запрос host и port (host=…, port=587), что позволяет серверу привязать токен к конкретному сервису, и у него есть внятный стандартный error-канал. Но раз почтовые клиенты в массе умеют XOAUTH2, включать нужно оба и не воевать с реальностью.
Критично важная деталь, на которой спотыкаются: в токене должен лежать access-token, а не id-token. Id-token — это JWT с aud = client_id, предназначенный самому клиентскому приложению, а не почтовому серверу. Он не даёт доступа к ресурсу и провалит вашу проверку aud. И scope access-токена обязан покрывать почтовые операции — если IdP выдаёт scope только под профиль пользователя, доступа к IMAP не будет.
Кто выдаёт токен: провайдер и его endpoints
Конструкция IdP-агностична — работает с Keycloak, Authentik, Azure AD, любым OIDC-совместимым провайдером. От него вам нужны несколько endpoint-ов: token endpoint (клиент получает access-token), introspection endpoint (RFC 7662 — Dovecot проверяет токен по сети), jwks_uri (набор публичных ключей для локальной проверки подписи) и device authorization endpoint (RFC 8628).
Про device authorization grant стоит сказать отдельно: для десктопных и мобильных почтовых клиентов без встроенного браузера это фактически единственный вменяемый флоу. Пользователь видит код, открывает его на любом устройстве с браузером, подтверждает — клиент забирает токен. Продвинутые клиенты вроде Thunderbird предпочитают authorization_code + PKCE (RFC 7636) со встроенным webview. Access-token обычно живёт 300–3600 секунд; refresh-token хранится в клиенте, живёт днями-неделями и тихо обновляет access за кулисами.
Валидация токена в Dovecot: introspection против локального JWT
Это ядро всей задачи. У Dovecot два принципиально разных пути проверки предъявленного токена.
Remote introspection. Dovecot на каждый логин (или с кэшем) дёргает introspection endpoint IdP и спрашивает: «этот токен ещё жив?». Провайдер отвечает JSON с active: true/false и claim-ами. Плюс — мгновенный revocation: отозвали токен на IdP, следующий логин отлетает. Минус — сетевой хоп на каждую аутентификацию и нагрузка на IdP, который становится точкой отказа для всей почты.
Local validation. Dovecot один раз тянет jwks_uri, кэширует публичные ключи и офлайн проверяет RS256-подпись JWT плюс claims — без единого сетевого запроса на логин (поддержка с Dovecot 2.3.11+). Плюс — нулевая latency и масштаб. Минус — revocation работает только по exp: отозванный, но ещё не протухший токен продолжит пускать в ящик, пока не истечёт.
Вне зависимости от пути обязательны четыре проверки claim-ов, и пропуск любой — это дыра:
`iss` — издатель токена должен совпадать с вашим доверенным IdP.
`aud` — audience должен содержать идентификатор почтового сервиса. Без проверки `aud` любой токен, выданный вашим IdP под любое приложение, открывает почту — классический confused deputy.
`exp` — токен не истёк.
`email`/`sub` — резолвится в существующий ящик. Без этой привязки токен Боба откроет ящик Алисы (token substitution).
Конфигурация Dovecot: auth-oauth2 на практике
Начинаем с механизмов. В /etc/dovecot/conf.d/10-auth.conf:
plain и login можно оставить на переходный период для клиентов без OAuth, но цель — их убрать. Теперь сам /etc/dovecot/conf.d/auth-oauth2.conf.ext с passdb, работающий по remote introspection:
А вот содержимое /etc/dovecot/dovecot-oauth2.conf.ext — здесь вся суть:
# Remote introspection (RFC 7662)
introspection_url = https://idp.evilmail.pro/realms/mail/protocol/openid-connect/token/introspect
introspection_mode = post
client_id = dovecot-imap
client_secret = <секрет клиента dovecot в IdP>
# claim, по которому решаем, что токен активен
active_attribute = active
active_value = true
# из какого claim берём имя пользователя (маппинг на ящик)
username_attribute = email
# доверенные издатели и обязательная проверка audience
issuers = https://idp.evilmail.pro/realms/mail
scope = email dovecot
tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt
Для локальной JWT-валидации без хопа на IdP introspection_url убирается, а Dovecot сам находит jwks_uri через discovery:
# Local validation: тянем jwks_uri из discovery, проверяем RS256 офлайн
local_validation = yes
openid_configuration_url = https://idp.evilmail.pro/realms/mail/.well-known/openid-configuration
Дальше — маппинг имени в системного пользователя. Токен даёт вам email, но письма лежат в /var/mail/vhosts/{domain}/{username}/ под uid/gid 5000. userdb должен превратить [email protected] в этот путь — обычно тем же userdb, что вы уже используете (SQL или static с mail=maildir:/var/mail/vhosts/%d/%n). Значение username_attribute из токена обязано резолвиться в существующий ящик, иначе аутентификация пройдёт, а доступа не будет. После правок — doveadm reload или полный рестарт, и сразу смотрим логи.
Postfix и SMTP submission: тот же токен на 587
SMTP не умеет OAuth сам — он делегирует аутентификацию Dovecot через SASL-сокет. В main.cf:
Механизм XOAUTH2 объявляется клиентам через тот же Dovecot auth-сокет: Postfix просто ретранслирует SASL-обмен в Dovecot и сам ничего не валидирует. В 10-master.conf Dovecot нужен слушатель сокета для Postfix:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
Submission поднимаем на 587 (STARTTLS) и 465 (implicit TLS) в master.cf. Принципиальный момент: это тот же самый access-token, что клиент предъявляет IMAP на 993 — при условии, что его scope покрывает отправку. Именно поэтому в конфиге выше стоит отдельный scope-компонент под mail: удобно иметь один scope на чтение и явно требовать право на send, чтобы скомпрометированный «read-only» токен не мог рассылать спам.
Отладка: openssl, doveadm и типичные грабли
Прежде чем подключать реального клиента, прогоните аутентификацию руками. Сгенерируйте base64 как показано выше и скормите её напрямую:
Декодируется в {"status":"401","schemes":"bearer","scope":""}. По протоколу клиент обязан ответить пустой строкой, чтобы получить финальный a1 NO. Второй инструмент — прямой тест passdb в обход сети:
И включите отладку в 10-logging.conf: auth_debug = yes, после чего в /var/log/dovecot.log будет видно, какой именно claim не сошёлся.
Грабли, на которые уходит больше всего времени:
Рассинхрон часов.exp/nbf считаются по UTC. Разъезд времени между mail-сервером и IdP больше допустимого leeway ломает валидацию наглухо — держите chrony/NTP на обоих.
Кэш introspection маскирует ротацию. Если держать кэш дольше TTL токена, отозванный токен продолжит пускать. Кэш introspection всегда ≤ TTL.
Клиент шлёт id-token вместо access. JWT с aud = client_id отлетает по проверке aud = mail. Смотрите aud/typ в декодированном токене.
`aud` не совпадает. Забыли добавить mail-сервис в audience на стороне IdP — все логины 401, хотя токен «валидный».
Capability mismatch. Клиент умеет только OAUTHBEARER, а вы объявили лишь XOAUTH2 (или наоборот). Всегда включайте оба.
Про клиентов честно: Thunderbird поддерживает OAuth2 (auth_code + PKCE) с произвольным IdP, но настраивается это вручную. Apple Mail и iOS работают только с захардкоженным списком провайдеров — свой Keycloak без MDM-профиля не подключить. Мобильные Gmail и Outlook живут только в своих экосистемах. Поэтому fallback-план для клиентов без OAuth (хотя бы app-password с коротким сроком жизни на выделенном порту) вам понадобится.
Чек-лист перед продакшеном
TLS на всех портах: 993, 587 (STARTTLS), 465 — токен никогда не должен лететь по plaintext.
Проверка aud, iss, exp включена и протестирована — особенно aud, без неё confused deputy.
Access-token TTL короткий (300–900 c); долгую сессию держит refresh-token в клиенте.
Кэш introspection не длиннее TTL токена.
Выбрана и задокументирована стратегия revocation (introspection для мгновенного, local — только по exp).
Rate-limit на auth-сокете против брутфорса протухших токенов.
Fallback-путь для клиентов без OAuth (Apple Mail, старые устройства).
Мониторинг всплесков 401 — это либо ротация ключей, либо разъехавшиеся часы, либо атака.
Ротация client_secret Dovecot↔IdP по расписанию.
Логирование sub/email каждой успешной аутентификации для аудита.
Проверен device authorization flow для десктоп/мобильных клиентов.
Отдельный scope на SMTP send, чтобы read-only токен не рассылал почту.
Токен-аутентификация не делает почтовый сервер неуязвимым — она меняет модель угроз: вместо вечного секрета, который надо защищать всю его жизнь, у вас короткоживущий токен, который сам протухает через пять минут. Для инфраструктуры, которую вы строите сегодня, это правильная точка отсчёта.