← Статьи

Exim + OpenLDAP: часть ящиков у себя, часть на Exchange

· ~6 мин чтения

Содержание

Ситуация знакомая многим: почтовый домен у компании один, а ящики живут в двух системах. Часть сотрудников сидит в 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:

  1. Адреса в LDAP нет — письмо отклоняется ещё на RCPT TO.
  2. У учётки стоит mailOnExchange: TRUE — письмо уходит на Exchange по SMTP.
  3. Во всех остальных случаях — доставка в локальный ящик через 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 нужны две вещи:

  1. Принятый домен example.kz с типом «Внутренний ретранслятор» (Internal Relay). Тогда Exchange не будет отклонять письма на адреса, которых у него нет, а отправит их дальше.
  2. Соединитель отправки для домена 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. Вопрос без регистрации — напишите мне напрямую.