среда, 5 октября 2022 г.

Nginx: basic auth через Samba AD

 

Введение

В данной статье описаны принцип работы и настройка basic auth в Nginx через Samba Active Directory.

Применение и принцип работы

Необходимость такого решения возникла в результате закрытия всех внутренних ресурсов от посторонних глаз. Но на каждый проект заводить отдельные .htpasswd файлы или делать один общий, а потом где-то хранить логин и пароль не очень хотелось – со временем проектов станет много и администрировать учётки станет не очень удобно. Тут-то на помощь и пришёл вариант использования Nginx с имеющейся Samba в качестве контроллера домена.

Для Nginx существует модуль ngx_http_auth_request_module.

Модуль ngx_http_auth_request_module (1.5.4+) предоставляет возможность авторизации клиента, основанной на результате подзапроса. Если подзапрос возвращает код ответа 2xx, доступ разрешается. Если 401 или 403 — доступ запрещается с соответствующим кодом ошибки. Любой другой код ответа, возвращаемый подзапросом, считается ошибкой.

То есть работа модуля основана на подзапросах к какому-либо стороннему сервису, который обращается к Active Directory, и возвращается к Nginx с ответом. Таковым сервисом является демон, написанный на Python, и называется ldap‑auth daemon. Поддерживает работу Python 2 и 3 версии.

На сайте Nginx изображена схема работы при использовании модуля авторизации и коннектора на Python:

The NGINX Plus reference implementation for LDAP authentication includes the ldap-auth daemon and a sample backend daemon

Необходимые зависимости

  • Модуль ngx_http_auth_request_module, согласно документации, не включен в сборку по умолчанию, но в пакетах из репозитория Nginx данный модуль присутствует. Для проверки необходимо вывести список всех модулей Nginx:
nginx -V 2>&1 | tr ' ' '\n' | grep with-http_auth_request_module
  • Для работы демона ldap‑auth daemon необходима библиотека python-ldap – установить её можно через пакетный менеджер python – pip. Но для этого потребуется установить на сервер следующие зависимости:
    • компилятор языка C
    • непосредственно python3
    • openldap-devel 
  • Помимо этого пригодится также пакет openldap-clients для тестирования подключения к LDAP-серверу в дальнейшем. Итого на сервер, где расположен Nginx, необходимо установить:
yum install python3 gcc python3-devel openldap-devel

Создание пользователя Active Directory

Для возможности обращения в AD с помощью ldap‑auth daemon необходима сервисная учётная запись с правами на чтение дерева.

  • Для создания в Microsoft AD или Samba можно воспользоваться оснасткой dsa.msc. Или же через консоль сервера в случае с Samba:
samba-tool user add nginx
  • Также необходимо отключить срок действия учетной записи:
samba-tool user setexpiry nginx --noexpiry

Проверка связанности с AD

  • Рекомендуется проверить напрямую через утилиту ldapsearch, есть ли возможность подключиться к AD с помощью созданной УЗ. Команда ниже выведет объекты дерева:
ldapsearch -v -D "nginx@domain.local" -w "PASSWORD" -b "DC=domain,DC=local" -H "ldap://10.10.4.30" | head -n 40

Если ошибок не возникло, можно переходить к установке демона.

Установка nginx-ldap-auth bare metal

  • Демон потребляет мало ресурсов, поэтому его допустимо установить на тот же сервер, где запущен Nginx:
curl -o /usr/bin/nginx-ldap-auth-daemon.py https://raw.githubusercontent.com/nginxinc/nginx-ldap-auth/master/nginx-ldap-auth-daemon.py
chmod +x /usr/bin/nginx-ldap-auth-daemon.py
  • Выполнить пробный запуск, задав адрес и порт:
/usr/bin/python3 /usr/bin/nginx-ldap-auth-daemon.py --host 127.0.0.1 --port 8080
  • Обратиться к демону через curl. В логах должно появиться что-то подобное:
curl localhost:8080
 
Start listening on 127.0.0.1:8080...
127.0.0.1 - - [02/Apr/2021 17:14:08] using username/password from authorization header
127.0.0.1 - - [02/Apr/2021 17:14:08] "GET / HTTP/1.1" 401 -
  • Убедившись, что запуск выполняется корректно, для удобного последующего запуска логично будет создать systemd-юнит:
cat > /etc/systemd/system/nginx-ldap-auth.service << EOF
[Unit]
Description=LDAP authentication helper for Nginx
After=network.target network-online.target
 
[Service]
Type=simple
User=nginx-ldap-auth
Group=nginx-ldap-auth
WorkingDirectory=/var/run
ExecStart=/usr/bin/python3 /usr/bin/nginx-ldap-auth-daemon.py --host 127.0.0.1 --port 8080
KillMode=process
KillSignal=SIGINT
Restart=on-failure
 
[Install]
WantedBy=multi-user.target
EOF

Стоит обратить внимание – в конфиге юнита указан отдельный пользователь nginx-ldap-auth, от имени которого будет запускаться демон. Необходимо создать данного пользователя:

adduser nginx-ldap-auth --no-create-home --shell=/bin/false
  • Выполнить запуск демона через systemd:
systemctl daemon-reload && systemctl start nginx-ldap-auth && systemctl enable nginx-ldap-auth
  • Проверить логи:
journalctl -u nginx-ldap-auth.service -f

Запуск в Docker

На гитхабе представлен Dockerfile для сборки Docker-образа nginx-ldap-auth. Я немного его изменил, добавив:

  • non-root пользователя для запуска приложения в контейнере
  • временную зону Europe/Moscow
  • 3 версию питона по умолчанию в entrypoint
  • аргументы CMD
ARG PYTHON_VERSION=3.9

FROM python:${PYTHON_VERSION}-alpine

RUN \
    addgroup -S nginx-ldap-auth && \
    adduser -S nginx-ldap-auth -G nginx-ldap-auth && \
    cp -r /usr/share/zoneinfo/Europe/Moscow /etc/localtime && echo Europe/Moscow > /etc/timezone && \
    apk --no-cache add openldap-dev && \
    apk --no-cache add --virtual build-dependencies build-base && \
    pip3 install python-ldap && \
    apk del build-dependencies

COPY nginx-ldap-auth-daemon.py /usr/src/app/
WORKDIR /usr/src/app/
EXPOSE 8888
USER nginx-ldap-auth
ENTRYPOINT ["python3", "/usr/src/app/nginx-ldap-auth-daemon.py"]
CMD ["--host", "0.0.0.0", "--port", "8888"]

Таким образом, можно выполнять запуск командой ниже:

docker build -t nginx-ldap-auth .
docker run --name nginx-ldap-auth -p 8888:8888 -d nginx-ldap-auth

А при необходимости запуска на порту, отличном от дефолтного 8888, можно изменить аргументы CMD при старте:

docker run --name nginx-ldap-auth -p 8888:8887 -d nginx-ldap-auth --host 0.0.0.0 --port 8887

Используемые заголовки

Демон nginx-ldap-auth успешно запущен, но перед дальнейшей настройкой непосредственно самого Nginx, необходимо убедиться, что запросы корректно приходят в Active Directory от nginx-ldap-auth. Для этого понадобится curl и специальные HTTP-заголовки, в которых будет передаваться вся необходимая информация для подключения к LDAP.

Параметры LDAPHTTP HeaderОписание
basednX-Ldap-BaseDNБаза поиска. В большинстве случаев соответствует суффиксу каталога. Если необходимо просто авторизовать пользователя, то не нужно указывать дополнительные группы, в которые этот пользователь входит (для этого используется template). Например, dc=domain,dc=local или cn=Users,dc=domain,dc=local
binddnX-Ldap-BindDNДля выполнения операции поиска в каталоге используется BIND DN, в данном параметре указывается непосредственно уникальное имя пользователя каталога. Например, cn=root,dc=domain,dc=local
bindpasswdX-Ldap-BindPassПароль пользователя 
cookienameX-CookieNameАвторизация на основе файлов кук, необязательный параметр. При использовании basic auth игнорируется.
realmX-Ldap-RealmИмя realm, необязательный параметр, используется по умолчанию значение Restricted.
templateX-Ldap-TemplateШаблон, по которому будет происходить выборка. Можно настраивать различные конфигурации. Например, для OpenLDAP подойдет такой шаблон: (cn=%(username)s), а для AD – (SAMAccountName=%(username)s) – это базовый шаблон, который просто выполняет поиск пользователя по каталогу.
urlX-Ldap-URLАдрес подключения к LDAP-серверу. Например, ldap://10.10.4.30:389 или ldaps://10.10.4.30:636

Проверка работы

Определившись с заголовками, можно выполнить проверку через curl, передав минимальные параметры: адрес AD, наименование домена, логин\пароль и простой шаблон:

curl --location --request GET 'http://localhost:8080' \
--header 'X-Ldap-URL: ldap://10.10.4.30' \
--header 'X-Ldap-BaseDN: DC=domain,DC=local' \
--header 'X-Ldap-BindDN: nginx@domain.local' \
--header 'X-Ldap-BindPass: STRONG_PASS' \
--header 'X-Ldap-Template: (SAMAccountName=%(username)s)' -vv -u nginx:STRONG_PASS
  • В ответ должен поступить 200 код, т.е. подключение успешно устанавливается от имени созданного пользователя:
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* Server auth using Basic with user 'nginx'
> GET / HTTP/1.1
> Authorization: Basic efVnsdfuZ2DpQaEFhDFsdxM0tMZVRmdWfg0
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> X-Ldap-URL: ldap://10.10.4.30
> X-Ldap-BaseDN: DC=domain,DC=local
> X-Ldap-BindDN: nginx@domain.local
> X-Ldap-BindPass: STRONG_PASS
> X-Ldap-Template: (SAMAccountName=%(username)s)
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: BaseHTTP/0.6 Python/3.6.8
< Date: Mon, 05 Apr 2021 10:01:42 GMT
<
* Closing connection 0

В случае ошибки вида Error while binding as search user: {‘msgtype’: 97, ‘msgid’: 1, ‘result’: 8, ‘desc’: ‘Strong(er) authentication required‘, ‘ctrls’: [], ‘info’: ‘BindSimple: Transport encryption required.’} необходимо добавить в конфиг smb.conf строку “ldap server require strong auth = no”, но делать так для production не рекомендуется.

Конфигурация Nginx

Наконец, можно выполнять настройку веб-сервера. Для удобства будет создана директория, в которой необходимо расположить конфиг для авторизации через AD – так его будет удобно подключать в необходимый контекст. Ну, или разместить по своему усмотрению:

mkdir -p /etc/nginx/conf.d/nginx-ldap-auth
 
cat > /etc/nginx/conf.d/nginx-ldap-auth/nginx-ldap-auth.conf << EOF
location = /auth {
internal;
#proxy_cache auth_cache;
proxy_cache_valid 200 10m;
proxy_cache_key "$http_authorization$cookie_nginxauth";
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass http://127.0.0.1:8080;
 
proxy_set_header X-Ldap-URL "ldap://10.10.4.30:389";
proxy_set_header X-Ldap-BaseDN "DC=domain,DC=local";
proxy_set_header X-Ldap-BindDN "nginx@domain.local";
proxy_set_header X-Ldap-BindPass "STRONG_PASS";
proxy_set_header X-Ldap-Template "(SAMAccountName=%(username)s)";
}
EOF

В файле nginx-ldap-auth.conf описан location, в котором настроено:

  • наименование location – в данном случае /auth
  • кеширование 200 кода (через пробел можно добавить и другие коды) ответа в течение 10 минут
  • проксирование запросов на локальный адрес, где слушает nginx-ldap-auth
  • передача HTTP-заголовков для LDAP

Если раскомментировать директиву “proxy_cache auth_cache”, то необходимо в nginx.conf в контексте http указать директиву proxy_cache_path cache/ keys_zone=auth_cache:10m; – она задаёт путь и другие параметры кэша. Данные кэша хранятся в файлах.

Поскольку тело запроса отбрасывается для подзапросов аутентификации, необходимо отключить директиву “proxy_pass_request_body”, а также установить для заголовка Content-Length пустую строку

  • И в завершение осталось подключить в нужном контексте “server” location для авторизации через Active Directory – выделено жирным:

server {
        server_name example.com
 
location / {
        auth_request /auth;
        proxy_pass http://10.16.0.14:81;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        autoindex on;
        }
 
        include /etc/nginx/conf.d/nginx-ldap-auth/nginx-ldap-auth.conf;
}
}

В вышеописанном конфигурационном файле к обычному “location /” добавляется “auth_request /auth;” и после подключается ранее сформированный файл nginx-ldap-auth.conf. Далее остаётся проверить синтаксис и выполнить релоад Nginx:

nginx -t && nginx -s reload

Исключение для basic auth

Удобным может тот случай, когда для доверенных адресов какую-либо авторизацию на веб-сервере можно вообще убрать – на помощь приходит директива “satisfy any”:

location / {
        satisfy any;
        allow 10.10.1.0/24;
        auth_request /auth;
        proxy_pass http://10.16.0.14:81;

Если пользователь пришёл с адреса из подсети 10.10.1.0/24, то satisfy разрешает доступ, если все (all) или хотя бы один (any) из модулей ngx_http_access_modulengx_http_auth_basic_modulengx_http_auth_request_module или ngx_http_auth_jwt_module разрешают доступ и в таком случае пароль вводить не понадобится.

Удобно сделать white-лист и подключать его через include:

location / {
        satisfy any;
        include /etc/nginx/conf.d/whitelist_ip.conf;
        auth_request /auth;
        proxy_pass http://10.16.0.14:81;
}

Заключение

По сути вся настройка сводится к тому, что команда Nginx предоставила готовое решение и описала общие принципы настройки, что очень удобно. К тому же, для большего удобства предоставлены Dockerfile для сборки образов и запуска nginx-ldap-auth в контейнере.

Из минусов мне видится два и причём весьма существенных:

  • если пропадёт сетевая связанность до сервера с AD или сам сервер падёт смертью храбрых, то клиент на Nginx не сможет авторизоваться и в ответ получит 500 код. Для серьезных решений это может быть критичным моментом и стоит это учитывать. Но в теории Nginx сейчас крайне функционален и наверняка можно обыграть этот нюанс с использованием модуля njs или хитрых конструкций из директив.
  • аутентификация в AD происходит по протоколу LDAP, а это по сути большая дыра. В идеале использовать хотя бы NTLM или Kerberos.

Также есть форк, в котором есть возможность использования нескольких LDAP-серверов, что в теории должно нивелировать описанный выше минус. Но на практике не проверял.

Используемые источники

Источник: здесь

Комментариев нет:

Отправить комментарий