NGINX Mail Proxy: на пути к INBOX

253f8d37b381bdc59dabfb106ac2e598

Перед нами загруженный почтовый сервер с заполненными почтовыми ящиками, большим почтовым трафиком и задача сделать с этим что-нибудь, так как письма «не ходят», а ещё 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=, method=PLAIN, rip=10.1.1.176, lip=10.1.1.81, mpid=8199, session=<4Ome7eMg9LQKAQGw>

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

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

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

© Habrahabr.ru