Exim + OpenLDAP: часть ящиков у себя, часть на Exchange
Содержание
Ситуация знакомая многим: почтовый домен у компании один, а ящики живут в двух системах. Часть сотрудников сидит в Exchange, остальные — на Linux-сервере с Dovecot. Входящая почта для всего домена приходит на один MX, и он должен решить, кому отдать каждое письмо.
Раньше у нас это решалось статическим списком адресов в transport-файле Postfix. Перевели человека в Exchange — не забудь поправить файл. Забыл — почта ушла не туда. Когда я заменял эту систему (iRedMail) на свою связку Exim + OpenLDAP + Dovecot + Rspamd, то перенёс признак «ящик на Exchange» прямо в учётную запись LDAP. Теперь маршрут меняется одним атрибутом, а конфиги трогать не нужно.
Схема
интернет
│ MX example.kz
▼
┌──────────────────────────┐
│ Exim (+ Rspamd) │
│ ищет адрес в OpenLDAP │
└───────┬──────────┬───────┘
mailOnExchange=TRUE │ обычный ящик
▼ ▼
Exchange Dovecot (LMTP)
По каждому получателю Exim спрашивает LDAP:
- Адреса в LDAP нет — письмо отклоняется ещё на
RCPT TO. - У учётки стоит
mailOnExchange: TRUE— письмо уходит на Exchange по SMTP. - Во всех остальных случаях — доставка в локальный ящик через LMTP Dovecot.
Все имена в статье условные: домен example.kz, база dc=example,dc=kz.
Атрибут в LDAP
Стандартного атрибута «где живёт ящик» в LDAP нет, поэтому я завёл свою маленькую схему: один атрибут и вспомогательный класс, который навешивается на уже существующие учётки.
# mailrouting.ldif
dn: cn=mailrouting,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: mailrouting
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1.1 NAME 'mailOnExchange'
DESC 'Mailbox is hosted on Exchange'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.99999.1.2.1 NAME 'mailRouting'
DESC 'Mail routing flags' AUXILIARY
MAY ( mailOnExchange ) )
Внимание
1.3.6.1.4.1.99999 — пример. Для боевой схемы получите собственный номер (Private Enterprise Number) в IANA — это бесплатно — или хотя бы выберите ветку, которая гарантированно не пересечётся с чужими схемами.
sudo ldapadd -Y EXTERNAL -H ldapi:/// -f mailrouting.ldif
Чтобы перевести пользователя на Exchange, ему добавляется класс и атрибут:
dn: uid=ivanov,ou=Users,dc=example,dc=kz
changetype: modify
add: objectClass
objectClass: mailRouting
-
add: mailOnExchange
mailOnExchange: TRUE
Проверяю:
ldapsearch -x -H ldap://127.0.0.1 -D "cn=exim,ou=Services,dc=example,dc=kz" -W \
-b "ou=Users,dc=example,dc=kz" "(mail=ivanov@example.kz)" mail mailOnExchange
Отдельная учётка для Exim
Пускать Exim в LDAP анонимно или под администратором я не хотел. Для него заведена своя учётка cn=exim,ou=Services,..., которой разрешено только читать нужные атрибуты:
olcAccess: to dn.subtree="ou=Users,dc=example,dc=kz"
attrs=mail,mailAlias,mailOnExchange,accountStatus,objectClass,entry
by dn.exact="cn=exim,ou=Services,dc=example,dc=kz" read
by * break
Пароли пользователей Exim видеть незачем: проверку SMTP AUTH можно отдать Dovecot.
Настройка Exim
Ниже фрагменты в формате основного конфига exim.conf. Если у вас Debian с разбитой конфигурацией (conf.d), те же блоки раскладываются по своим файлам: макросы и списки — в main, роутеры — в router, транспорты — в transport.
Подключение к LDAP
ldap_default_servers = 127.0.0.1::389
LDAP_BIND = user="cn=exim,ou=Services,dc=example,dc=kz" pass=СЕКРЕТ
LDAP_USERS = ou=Users,dc=example,dc=kz
# Активный пользователь с таким адресом
LDAP_FILTER = (&(objectClass=mailUser)(accountStatus=active)(mail=${quote_ldap:$local_part@$domain}))
quote_ldap здесь обязателен. Без экранирования адрес вроде *)(mail=* превращает поиск в LDAP-инъекцию.
Совет
Файл с паролем сервисной учётки закройте правами: chown root:Debian-exim, chmod 640.
Свои домены
domainlist local_domains = example.kz
Если доменов много и они тоже лежат в LDAP, список можно строить запросом. Но для пары доменов статический список проще и быстрее.
Проверка получателя в ACL
Неизвестных получателей я отбиваю сразу, ещё до приёма тела письма. Тогда сервер не рассылает отчёты о недоставке (backscatter) на поддельные адреса отправителей:
acl_check_rcpt:
# ... стандартные проверки ...
deny domains = +local_domains
!verify = recipient
message = Unknown user
verify = recipient прогоняет адрес через роутеры, которые описаны ниже. Если ни один из них адрес не принял, отправитель получит 550 Unknown user.
Роутеры
Тут важен порядок: Exim проверяет роутеры сверху вниз, и письмо забирает первый подошедший.
begin routers
# 1. Ящик на Exchange
exchange_mailbox:
driver = manualroute
domains = +local_domains
condition = ${lookup ldap {LDAP_BIND \
ldap:///LDAP_USERS?mailOnExchange?sub?LDAP_FILTER} \
{${if eq{$value}{TRUE}}}{no}}
route_list = * exchange.example.kz byname
transport = remote_smtp
no_more
# 2. Локальный ящик в Dovecot
local_mailbox:
driver = accept
domains = +local_domains
condition = ${lookup ldap {LDAP_BIND \
ldap:///LDAP_USERS?mail?sub?LDAP_FILTER} \
{yes}{no}}
transport = dovecot_lmtp
no_more
Пара тонкостей:
${if eq{$value}{TRUE}}— в LDAP булев атрибут хранится строкойTRUE/FALSE. Если атрибута у учётки нет вовсе, lookup ничего не найдёт, сработает ветка{no}, и письмо пойдёт дальше, к локальному роутеру.no_moreстоит у обоих роутеров. Если адрес наш, но ни один из них не подошёл, Exim дальше искать не будет, иverify = recipientвернёт отказ.
Алиасы и пересылки добавляются ещё одним роутером redirect, который ставится перед этими двумя: поиск по атрибуту mailAlias с тем же фильтром по адресу.
Транспорт в Dovecot
begin transports
dovecot_lmtp:
driver = lmtp
socket = /run/dovecot/lmtp
batch_max = 200
В Dovecot сокет LMTP должен быть доступен пользователю Exim:
# /etc/dovecot/conf.d/10-master.conf
service lmtp {
unix_listener lmtp {
mode = 0660
user = dovecot
group = Debian-exim
}
}
Транспорт remote_smtp для Exchange обычный, из конфига по умолчанию. Если Exchange в той же сети и принимает только TLS, добавьте в него hosts_require_tls.
Что настроить в Exchange
Чтобы почта ходила в обе стороны, в Exchange нужны две вещи:
- Принятый домен
example.kzс типом «Внутренний ретранслятор» (Internal Relay). Тогда Exchange не будет отклонять письма на адреса, которых у него нет, а отправит их дальше. - Соединитель отправки для домена
example.kz(или для всей исходящей почты) с промежуточным узлом — нашим Exim. Письмо от сотрудника в Exchange коллеге с ящиком в Dovecot уйдёт на Exim, а тот доставит его локально.
На стороне Exim IP-адрес Exchange добавляется в relay_from_hosts, чтобы через него можно было отправлять почту наружу.
Осторожно
Следите, чтобы адрес не числился одновременно и в Exchange, и как локальный ящик без флага. Иначе Exchange отправит письмо на Exim, Exim вернёт на Exchange — и получится петля, которую оборвёт только счётчик Received-заголовков.
Проверка до запуска
За что я особенно люблю Exim — маршрут любого адреса можно проверить, не отправив ни одного письма.
# Куда пойдёт письмо?
exim -bt ivanov@example.kz
# ivanov@example.kz
# router = exchange_mailbox, transport = remote_smtp
# host exchange.example.kz [10.0.0.20]
exim -bt petrov@example.kz
# petrov@example.kz
# router = local_mailbox, transport = dovecot_lmtp
exim -bt nobody@example.kz
# nobody@example.kz is undeliverable: Unrouteable address
Если адрес пошёл не туда, куда ожидалось, включаю отладку только для поиска. Exim покажет точный LDAP-запрос и ответ:
exim -d+lookup -bt ivanov@example.kz 2>&1 | grep -A3 -i ldap
А весь SMTP-диалог без реальной доставки можно посмотреть в режиме -bh — он имитирует подключение с заданного IP:
exim -bh 203.0.113.10
Что в итоге
- Перевести сотрудника между Exchange и Linux-ящиком — это поменять один атрибут в LDAP. Без правки конфигов и без перезапуска Exim.
- Неизвестные адреса отбиваются на
RCPT TO, никакого backscatter. - Куда пойдёт письмо на любой адрес, видно за секунду через
exim -bt.
Флагом я управляю не через ldapmodify, а через свою небольшую веб-панель. Но это уже тема для отдельной статьи.
Обсуждение
Комментарии работают через GitHub: для ответа нужен аккаунт GitHub. Вопрос без регистрации — напишите мне напрямую.