Application: Nginx, Python, LDAP.
Задача: обеспечить аутентификацию пользователей сайта через внешний LDAP-сервис, средствами web-сервера "Nginx".
С точки зрения эксплуатационщика на предприятии удобно проводить аутентификацию пользователей внутренних сервисов через некий централизованный каталог с учётными данными. Чаще всего для этого применяется сервис "Microsoft Active Directory", но технически почти всегда в этой роли можно использовать иные реализации LDAP, вроде "OpenLDAP" или "389-DS".
В сложносочинённых сайтах задачи аутентификации и авторизации обычно возлагаются на специально для этого предназначенные компоненты внутри информационной системы, для реализации которых во всех распространённых языках программирования имеются библиотеки функций взаимодействия с LDAP. С мелкими сайтами, возможно даже полностью статическими, сложнее - у них нет подкапотных механизмов, посредством которых можно было бы проверять право доступа пользователя к запрашиваемым данным. В таком случае на роль привратника остаётся один кандидат - web-браузер. И вот на этом этапе выясняется, что любимый нами "Nginx" не имеет модуля аутентификации через LDAP.
Как вариант решения поставленной задачи, можно воспользоваться системных модулем "auth_pam", указав "Nginx" проводить аутентификацию через PAM несущей операционной системы ("Linux" или xBSD), в которой настроена связка с LDAP. Лет пять назад разрабатывалась реализация в виде модуля "nginx-auth-ldap", но он устарел сейчас разработчики "Nginx" официально предлагают использовать другой подход, с промежуточным сервисом "nginx-ldap-auth-daemon".
Предлагаемая схема взаимодействий проста и прозрачна. Когда пользователь обращается к защищённому разделу сайта, web-сервер "Nginx" запрашивает посредством протокола "HTTP Basic authentication" логин и пароль. Полученные аутентификационные данные встроенным модулем "http_auth_request" сразу отправляются по протоколу проксирования фоновому web-сервису "LDAP Auth Daemon", который в свою очередь обращается к указанному в его настройках LDAP-серверу за подтверждением существования пользователя с предъявленными логином и паролем. Положительный или отрицательный ответ доставляется обратно по цепочке web-серверу "Nginx", который допускает или нет пользователя до запрашиваемого контента. Этот процесс красиво расписан в официальной документации.
Перейдём же к практике.
Установка "Nginx LDAP Auth Daemon".
Простенькое приложение-посредник написано на "Python", который почти всегда уже установлен:
# aptitude install python2.7-minimal python-ldap git
Загружаем дистрибутив "Nginx LDAP Auth Daemon":
# cd /usr/src
# git clone https://github.com/nginxinc/nginx-ldap-auth
# git clone https://github.com/nginxinc/nginx-ldap-auth
Копируем единственный нужный исполняемый файл в наиболее подходящее ему место в файловой системе:
# cp ./nginx-ldap-auth/nginx-ldap-auth-daemon.py /usr/local/bin/
Заготавливаем место для журналов событий (аутентификация дело ответственное):
# mkdir -p -m 740 /var/log/nginx-ldap-auth
# chown -R www-data /var/log/nginx-ldap-auth
# chown -R www-data /var/log/nginx-ldap-auth
Пробуем запустить приложение:
# cd /usr/local/bin
# LOG=/var/log/nginx-ldap-auth/daemon.log ./nginx-ldap-auth-daemon.py start
# LOG=/var/log/nginx-ldap-auth/daemon.log ./nginx-ldap-auth-daemon.py start
По умолчанию "Nginx LDAP Auth Daemon" прослушивает запросы только на "локальной сетевой петле", на обычно свободном порту, что меня полностью устраивает и освобождает от необходимости что-то настраивать:
# netstat -apn | grep -i python
tcp ... 127.0.0.1:8888 0.0.0.0:* LISTEN .../python
Оставим пока приложение запущенным и перейдём к настройке web-сервера.
Настройка аутентификации через LDAP в "Nginx".
Принимая за данность, что web-сервер "Nginx" уже корректно настроен, дополним настройки конкретного сайта блоком параметров проксирования запросов аутентификации к "Nginx LDAP Auth Daemon":
# vi /etc/nginx/sites-available/site.example.net.conf
....
server {
....
# Перечисляем требования, которым должны удовлетворять пользователи сайта
satisfy all;
#
# (пользователи из локальных сетей)
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
#
# (прошедшие аутентификацию)
auth_request /auth-proxy;
# Блок параметров запроса к "Nginx LDAP Auth Daemon"
location = /auth-proxy {
internal;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass http://127.0.0.1:8888;
proxy_set_header X-Ldap-URL "ldaps://ldap.example.net:636";
proxy_set_header X-Ldap-Template "(&(uid=%(username)s)(objectclass=person)(memberOf=cn=web-service-users,ou=groups,dc=example,dc=net))";
proxy_set_header X-Ldap-BaseDN "ou=People,dc=example,dc=net";
proxy_set_header X-Ldap-BindDN "uid=nginx-web-service,ou=Accounts,ou=Services,dc=example,dc=net";
proxy_set_header X-Ldap-BindPass "***";
}
....
}
....
server {
....
# Перечисляем требования, которым должны удовлетворять пользователи сайта
satisfy all;
#
# (пользователи из локальных сетей)
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
#
# (прошедшие аутентификацию)
auth_request /auth-proxy;
# Блок параметров запроса к "Nginx LDAP Auth Daemon"
location = /auth-proxy {
internal;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass http://127.0.0.1:8888;
proxy_set_header X-Ldap-URL "ldaps://ldap.example.net:636";
proxy_set_header X-Ldap-Template "(&(uid=%(username)s)(objectclass=person)(memberOf=cn=web-service-users,ou=groups,dc=example,dc=net))";
proxy_set_header X-Ldap-BaseDN "ou=People,dc=example,dc=net";
proxy_set_header X-Ldap-BindDN "uid=nginx-web-service,ou=Accounts,ou=Services,dc=example,dc=net";
proxy_set_header X-Ldap-BindPass "***";
}
....
}
....
Конструкция "%(username)s" в примере выше используется для получения из набора внутренних переменных "Nginx" имени пользователя (логина), введённого в форму "HTTP Basic authentication".
Проверяем синтаксическую корректность конфигурации "Nginx" и применяем её:
# nginx -t && nginx -s reload
Сразу после запуска можно пробовать обращаться к закрытой части сайта и проверять, работает ли связка "Nginx", "Nginx LDAP Auth Daemon" и LDAP в соответствии с ожиданиями. Если что-то пошло не так, смотрим предусмотрительно заготовленный журнал событий.
Подключение к LDAP с "самоподписанным" SSL-сертификатом.
Почти наверняка LDAP-сервер, работающий только в локальной сети, для шифрования соединений будет предлагать "self-signed" SSL-сертификат. Типовое python-приложение доверяет сертификатам, размещённым в соответствующем хранилище несущей операционной системы, так что самым простым способом наладить связь будет добавление публичной части самодельного сертификата LDAP-сервиса в перечень "доверенных":
# cp ./ss-rootCA.crt /usr/local/share/ca-certificates/
# update-ca-certificates
# update-ca-certificates
Последующая проверка (с несущего "Nginx LDAP Auth Daemon" сервера) не должна демонстрировать предупреждений о проблемах распознавания подписи SSL-сертификата целевого сервера:
$ echo QUIT | openssl s_client -connect ldap.exfmple.net:636 -CApath /etc/ssl/certs
О кешировании запросов аутентификации.
Аутентификация через "auth_request" в "Nginx" работает так, что на каждый запрос любого ресурса (даже мелкой картинки в составе страницы) генерируется внутреннее событие требования аутентификации, по которому отправляется запрос в промежуточную подсистему "ldap‑auth", которая в свою очередь обращается за подтверждением подлинности пользователя в LDAP. Таким образом на каждый запрос web-страницы создаётся от десятка до нескольких сотен подзапросов, в которых совершенно нет необходимости. Для снижения затрат ресурсов на такую непродуктивную деятельность в "Nginx" применяется кеширование.
Вначале на уровне глобальной конфигурации "Nginx" указываем желаемое месторасположение директории для файлов данных кешируемых запросов и максимальное время хранения таковых, а после в конфигурации сайта, для доступа к которому требуется аутентификация через LDAP, добавляем указание кешировать успешно завершённые запросы к подсистеме "ldap-auth" на срок до десяти минут:
# vi /etc/nginx/sites-available/site.example.net.conf
proxy_cache_path /var/lib/nginx/cache/ keys_zone=auth_cache:10m;
....
server {
....
location = /auth-proxy {
internal;
proxy_cache auth_cache;
proxy_cache_valid 200 10m;
....
....
server {
....
location = /auth-proxy {
internal;
proxy_cache auth_cache;
proxy_cache_valid 200 10m;
....
# nginx -t && nginx -s reload
Настройка автозапуска "Nginx LDAP Auth Daemon" посредством "Systemd".
Ранее мы запустили приложение-прослойку вручную. Автоматизируем эту процедуру, описав подсистему аутентификации как постоянно работающий простой сервис:
# vi /etc/systemd/system/nginx-ldap-auth-daemon.service
[Unit]
Description=LDAP authentication helper for Nginx
After=network.target network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/run
Environment=LOG=/var/log/nginx-ldap-auth/daemon.log
ExecStart=/usr/local/bin/nginx-ldap-auth-daemon.py
KillMode=process
KillSignal=SIGINT
Restart=on-failure
[Install]
WantedBy=multi-user.target
Description=LDAP authentication helper for Nginx
After=network.target network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/run
Environment=LOG=/var/log/nginx-ldap-auth/daemon.log
ExecStart=/usr/local/bin/nginx-ldap-auth-daemon.py
KillMode=process
KillSignal=SIGINT
Restart=on-failure
[Install]
WantedBy=multi-user.target
Указываем "Systemd" перечитать и принять новую конфигурацию, а также явно активируем и запускаем новый сервис:
# systemctl daemon-reload
# systemctl enable nginx-ldap-auth-daemon.service
# systemctl start nginx-ldap-auth-daemon
# systemctl enable nginx-ldap-auth-daemon.service
# systemctl start nginx-ldap-auth-daemon
Журналы событий нам в помощь, если что-то работает не так, как задумывалось:
# systemctl status nginx-ldap-auth-daemon
# journalctl -xe
# journalctl -xe
Наладка ротации журналов событий.
Выше упоминалось, что реализация аутентификации в "Nginx" склонна генерировать массу запросов, которые записываются в журнал событий. Если забыть об этом, то через полгода в файловой системе вырастет огромный ненужный файл. Заранее натравим на него подсистему усечения и оборота журналов:
# vi /etc/logrotate.d/nginx-ldap-auth-daemon
/var/log/nginx-ldap-auth/*.log {
monthly
rotate 12
compress
delaycompress
missingok
notifempty
copytruncate
su www-data www-data
}
monthly
rotate 12
compress
delaycompress
missingok
notifempty
copytruncate
su www-data www-data
}
Проверяем корректность конфигурации, не воздействуя при этом на файлы журналов:
# logrotate -d /etc/logrotate.d/nginx-ldap-auth-daemon