NGINX Mail Proxy: на пути к INBOX
Перед нами загруженный почтовый сервер с заполненными почтовыми ящиками, большим почтовым трафиком и задача сделать с этим что-нибудь, так как письма «не ходят», а ещё Sieve еле шевелится. Предположим, что докинуть ядер/дисков не получится, а сделать что-то нужно.
Можно развернуть архивный почтовый сервер, перекинуть туда письма старше N лет, сделать скрипты перемещения на основном, но это не для нас. Мы будем делать более элегантное решение, которое позволит нам в будущем упростить расширение почтовой инфраструктуры.
Для Nginx существует модуль mail, который позволяет проксировать и шифровать почтовые подключения, но это ещё не всё. Также имеется возможность авторизовать пользователя и перенаправить соединение. Осталось только завести базу пользователей и перенаправить трафик через прокси — выглядит отлично.
Подключения клиентов imap на сервер вида example.com
чаще всего устанавливаются через считывание AutoDiscover/Autoconfig, либо SRV записи с imap сервером указанным в соответствии с RFC 6186. В противном случае домен imap указывается вручную, обычно как imap.example.com
.
Мы рассмотрим простейший пример, когда клиент будет подключаться напрямую к прокси, и, в зависимости от записи в таблице (user; mail[n].example.com; port), будет происходить маршрутизация клиента на его почтовый сервер.
Техническое решение выбрано, опишем задачу:
Развернуть nginx-proxy с модулем mail
Написать скрипт авторизации пользователей
Развернуть почтовый сервер
ПРИМЕЧАНИЕ: Это не законченное корпоративное решение, я не буду детально рассматривать в статье вопросы безопасности, просто 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 для проверки работы нашего «маршрутизатора».
Основное преимущество такого подхода к проксированию пользователей заключается в том, что он позволяет гибко добавлять новые почтовые серверы с минимальным воздействием на существующую инфраструктуру. Например, можно постепенно переводить пользователей на новый сервер, наращивать количество серверов, при этом сохраняя непрерывность работы.