Перед нами загруженный почтовый сервер с заполненными почтовыми ящиками, большим почтовым трафиком и задача сделать с этим что-нибудь, так как письма "не ходят", а ещё Sieve еле шевелится. Предположим, что докинуть ядер/дисков не получится, а сделать что-то нужно.

Можно развернуть архивный почтовый сервер, перекинуть туда письма старше N лет, сделать скрипты перемещения на основном, но это не для нас. Мы будем делать более элегантное решение, которое позволит нам в будущем упростить расширение почтовой инфраструктуры.

Для Nginx существует модуль mail, который позволяет проксировать и шифровать почтовые подключения, но это ещё не всё. Также имеется возможность авторизовать пользователя и перенаправить соединение. Осталось только завести базу пользователей и перенаправить трафик через прокси - выглядит отлично.

Подключения клиентов imap на сервер вида example.com чаще всего устанавливаются через считывание AutoDiscover/Autoconfig, либо SRV записи с imap сервером указанным в соответствии с RFC 6186. В противном случае домен imap указывается вручную, обычно как imap.example.com.

Мы рассмотрим простейший пример, когда клиент будет подключаться напрямую к прокси, и, в зависимости от записи в таблице (user; mail[n].example.com; port), будет происходить маршрутизация клиента на его почтовый сервер.

Техническое решение выбрано, опишем задачу:

  1. Развернуть nginx-proxy с модулем mail

  2. Написать скрипт авторизации пользователей

  3. Развернуть почтовый сервер

ПРИМЕЧАНИЕ: Это не законченное корпоративное решение, я не буду детально рассматривать в статье вопросы безопасности, просто proof-of-concept.

Итак, начнём.

Собираем NGINX из исходников с модулем mail

Дальнейшие действия проводятся на Debian 12.

По умолчанию, NGINX не поставляется с модулем mail (за это нужно доплатить), так что мы будем собирать его руками.

Для начала поставим зависимости:

apt install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev

Скачаем исходники последней версии:

NGINXLATEST=$(wget -qO- https://nginx.org/en/download.html | grep -oP 'nginx-\K[\d.]+(?=\.tar\.gz)' | head -1)
wget https://nginx.org/download/nginx-${NGINXLATEST}.tar.gz
tar -zxvf nginx-${NGINXLATEST}.tar.gz
cd nginx-${NGINXLATEST}

Добавим модуль mail:

./configure --with-mail --with-mail_ssl_module

Соберем NGINX (на 1 ядре 1 гиге заняло <30 секунд) и установим его в систему:

make
make install

Важно! NGINX устанавливается в папку /usr/local/nginx/

Сервис для NGINX заводим ручками:

nano /etc/systemd/system/nginx.service
[Unit]
Description=NGINX Web Server
After=network.target

[Service]
Type=forking
ExecStartPre=/usr/local/nginx/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/local/nginx/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/local/nginx/sbin/nginx -s reload -g 'daemon on; master_process on;'
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PIDFile=/usr/local/nginx/logs/nginx.pid
Restart=on-failure
KillMode=mixed

[Install]
WantedBy=multi-user.target

Переходим к конфигурации:

nano /usr/local/nginx/conf/nginx.conf
worker_processes auto;

mail {

    server_name example.com;
    # Путь до нашего скрипта с авторизацией
    auth_http 127.0.0.1:9000/auth;
    proxy_pass_error_message on;

     server {
        listen   143;
        protocol imap;
    }
}

Запускаем сервис:

systemctl start nginx.service

Переходим к идентификации

Теперь займемся скриптом идентификации, нам нужно соответствовать спецификации NGINX Mail Auth. В качестве бекенда будем использовать flask.

nano mail_auth.py
from flask import Flask, request, jsonify, abort, make_response
import logging

app = Flask(__name__)

# настроим логгирование
logging.basicConfig(level=logging.INFO)

# Define your mapping table with ports
user_server_map = {
    "user1": ("mail1.example.com", 143),
    "user2": ("mail1.example.com", 143),
    "user3": ("192.0.2.1", 143)
}

# Ограничим количество попыток авторизации
MAX_LOGIN_ATTEMPTS = 10

@app.before_request
def log_request_info():
    logging.info('Request Path: %s', request.path)
    logging.info('Request Method: %s', request.method)
    logging.info('Request Headers: %s', request.headers)
    logging.info('Request Remote Address: %s', request.remote_addr)

@app.route('/auth', methods=['GET'])
def auth():
    # Читаем заголовки
    auth_method = request.headers.get('Auth-Method')
    auth_user = request.headers.get('Auth-User')
    auth_pass = request.headers.get('Auth-Pass')
    auth_protocol = request.headers.get('Auth-Protocol')
    auth_login_attempt = int(request.headers.get('Auth-Login-Attempt', '1'))

    # Проверим валидность клиента
    if auth_user not in user_server_map or not auth_user or not auth_pass:
        if auth_login_attempt > MAX_LOGIN_ATTEMPTS:
            # Fail2ban
            logging.info(f"User {auth_user} exceeded max login attempts.")
            response = make_response()
            response.headers['Auth-Status'] = 'Invalid login or password'
            return response

        # Ответ в случае некорректных данных авторизации
        response = make_response()
        response.headers['Auth-Status'] = 'Invalid login or password'
        response.headers['Auth-Wait'] = '3'  # ждем 3 секунды до следующей попытки
        return response

    # Ищем сервер
    server, port = user_server_map.get(auth_user, ("127.0.0.1", 143))
    logging.info(f"Authenticating user {auth_user} with server {server} on port {port}")

    # Возвращаем сервер и порт для клиента
    response = make_response()
    response.headers['Auth-Status'] = 'OK'
    response.headers['Auth-Server'] = server
    response.headers['Auth-Port'] = str(port)
    return response

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=9000, debug=True)

Переходим к конфигурации Dovecot

Установим пакет из репозитория:

apt install dovecot-core dovecot-imapd

Создадим пользователя и группу:

groupadd -g 5000 vmail
useradd -g vmail -u 5000 vmail -d /opt/demomail -m
chown vmail:vmail /opt/demomail

Сделаем минимальную конфигурацию:

mv /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.bak
nano /etc/dovecot/dovecot.conf
protocols = imap
listen = *
mail_location = maildir:/opt/demomail/%u
ssl = no
auth_mechanisms = plain
disable_plaintext_auth = no
mail_privileged_group = vmail

passdb {
  driver = passwd-file
  args = /etc/dovecot/conf.d/dovecot.passwd
}

userdb {
  driver = static
  args = uid=vmail gid=vmail home=/opt/demomail/%u
}

log_path = /var/log/dovecot.log
info_log_path = /var/log/dovecot-info.log

Добавим пользователя на наш почтовый сервер:

nano /etc/dovecot/conf.d/dovecot.passwd
user1:{PLAIN}password123    - для 1го сервера
user2:{PLAIN}password123    - для 2го сервера

Перезапускаем сервис:

systemctl restart dovecot.service

Проверяем работу сервиса

Запускаем наш скрипт на сервере указанном в nginx.conf:

python3 mail_auth.py

И пробуем подключиться:

telnet example.com 143

Нас приветствует NGINX Mail Proxy

Escape character is '^]'.
* OK IMAP4 ready

Вводим данные:

1 LOGIN user1 password123

Супер, мы попали на нужный нам почтовый сервер

1 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW STATUS=SIZE SAVEDATE LITERAL+ NOTIFY] Logged in

В логах /var/log/dovecot-info.log отобразилось

Aug 30 10:19:36 imap-login: Info: Login: user=<user1>, method=PLAIN, rip=10.1.1.176, lip=10.1.1.81, mpid=8199, session=<4Ome7eMg9LQKAQGw>

Для отключения можно ввести Ctrl + ] и нажать Enter

Резюмируя, в статье мы рассмотрели базовую настройку NGINX Mail Proxy сервера, написали простой скрипт для идентификации пользователей и развернули простейший сервер imap с использованием Dovecot для проверки работы нашего "маршрутизатора".

Основное преимущество такого подхода к проксированию пользователей заключается в том, что он позволяет гибко добавлять новые почтовые серверы с минимальным воздействием на существующую инфраструктуру. Например, можно постепенно переводить пользователей на новый сервер, наращивать количество серверов, при этом сохраняя непрерывность работы.

Комментарии (5)


  1. WhiteApfel
    04.09.2024 06:55

    Статья имеет место быть как описание фичи, но вопрос, как это решает озвученную проблему, что есть сервер, которому плохо и на котором "не ходят" письма? Трафика на сервере меньше не стало, появилось только ещё одно промежуточное звено. Проблему решают дополнительные сервера


    1. denplab Автор
      04.09.2024 06:55

      Спасибо за комментарий, действительно, стоило подробнее описать архитектуру решения. У меня в планах написать ещё одну статью по теме, чтобы раскрыть не только распределение пользователей по почтовым серверам, но и описать работу почтовых шлюзов в сочетании с mail proxy для доставки писем. В контексте данной статьи я показал именно то, что вы описываете, развёртывание дополнительного сервера без влияния на текущую инфраструктуру (Подразумевается, что у вас уже есть настроенная почтовая система, присутствует некий pipeline для обработки писем, но, например, закончилось место для ящиков или медленно работает поиск по письмам). Если вы хотите избавиться от единой точки отказа или распределить трафик по серверам, вы можете развернуть N прокси с nginx, сделать Round-robin DNS для них. Если попытаться развернуть ещё один почтовый сервер без использования решения для маршрутизации пользователей, как, например, описанного в этой статье, вы столкнётесь с синхронизацией содержимого почтовых ящиков, что не всегда может быть удобно. В случае же "умного" проксирования, каждый из почтовых серверов будет загружен только теми пользователями и задачами, которые непосредственно на нём присутствует, что в конечном счёте позволит использовать меньше ресурсов для достижения той же цели.


      1. ildarz
        04.09.2024 06:55

        Сильно понятнее не стало. :) Во-первых, любая вменяемая корпоративная почтовая система уже имеет то самое "решение для маршрутизации писем" (оно же MTA) в качестве составной части, и дополнительный сервер для хранения новых п/я обычно вполне себе прозрачно разворачивается штатно.

        Во-вторых, задачу "медленно работает поиск по письмам" даже добавление доп. сервера само по себе не способно решить принципиально, потому что письма уже лежат там, где лежат, и от того, что новые вы начнете класть куда-то еще, с имеющимися ситуация не изменится. И как тогда в итоге ваше элегантное решение заменит "архивный почтовый сервер, перекинуть туда письма старше N лет, сделать скрипты перемещения на основном, но это не для нас" (с), о чем вы прямо с начала пишете? :)

        В-третьих:

        В случае же "умного" проксирования, каждый из почтовых серверов будет загружен только теми пользователями и задачами, которые непосредственно на нём присутствует, что в конечном счёте позволит использовать меньше ресурсов для достижения той же цели.

        Нет. Роль сервера п/я и так загружена "только пользователями, которые на нем присутствуют", ибо других п/я на нем просто нет. :) А роли МТА или клиентского доступа - вообще отдельный слой, могущий работать и на тех же серверах, и отдельно, и вопрос, выгоднее ли их комбинировать с мэйлбоксами или разносить отдельно, сильно зависит от конкретики, тут нет какого-то единственно верного решения, которое позволит "использовать меньше ресурсов".

        По сути вы начинаете описывать сценарий "у нас был одинокий почтовик-комбайн, масштабирование никто не продумывал и не закладывал, что же теперь делать?"


        1. denplab Автор
          04.09.2024 06:55

          Да! Основной вопрос как раз к вменяемости корпоративной почтовой системы :)

          В общем случае не во всех почтовых серверах имеются возможности масштабирования из коробки, и, просто развернуть ещё один сервер для почтовых ящиков не всегда возможно, отсюда и упоминание "синхронизации" в комментарии выше.

          Не рассматривал в статье MTA, так как доставку писем на нужный сервер можно реализовать множеством способов и без Nginx. Речь идет именно о пользовательских почтовых ящиках и распределению пользователей по серверам, с минимальным вмешательством. Это как раз позволяет сделать imap прокси. При этом мы достигаем большой гибкости и не становимся зависимыми от платформенных почтовых решений, что может быть критично.

          Также нужно уточнить, что всё равно потребуется перенос ящиков (если у нас уже перегруженный сервер) при добавлении нового почтового сервера, однако, можно будет отложить\избежать создания архивного сервера, оставив принцип "одного окна" для пользователей.

          Надеюсь стало немного понятнее)


  1. werter_l
    04.09.2024 06:55

    Спасибо.

    Вот бы в nginx proxy manager завезли поддержку mail.