Интегрируйте аутентификацию LDAP с модулем auth_request Nginx.

Go Nginx

предисловие

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

  • Например, в бесплатной версии ELK на Kibana нет аутентификации личности;
  • Например, Open-Falcon версии 0.1 не сертифицирован для Dashboard;
  • Или некоторые веб-сайты, которые изначально были открыты для публики, вдруг перестали открываться для публики в определенные особые дни и в определенное особое время. . .

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

Это хороший способ, но базовая HTTP-аутентификация все-таки слишком проста, и неудобно интегрировать внешние источники аутентификации, такие как LDAP.

Таким образом, более гибкое решение — модуль auth_request Nginx.

Модуль auth_request для Nginx

auth_request — это запрос конкретной службы при посещении пути, защищенного auth_reuqest в Nginx. В соответствии с кодом состояния, возвращаемым этой службой, модуль auth_request делает следующий шаг, разрешая доступ или перенаправление, чтобы отпрыгнуть или что-то в этом роде. Таким образом, мы можем настроить все наши индивидуальные потребности на нем.

Предполагая, что наша среда — Centos, установка nginx в yum опущена. Поскольку nginx, установленный через yum и т. д., по умолчанию не компилирует модуль auth_request. Нам нужно перекомпилировать.

беги первымnginx -Vчтобы получить текущийnginxпараметры компиляции

# nginx -V
nginx version: nginx/1.14.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC) 
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'

Сначала установите некоторые зависимости

yum -y install gcc gcc-c++ autoconf automake make
yum -y install zlib zlib-devel openssl 
yum -y install openssl-devel pcre pcre-devel
yum -y install libxslt-devel
yum -y install redhat-rpm-config
yum -y install gd-devel
yum -y install perl-devel perl-ExtUtils-Embed
yum -y install geoip-devel
yum -y install gperftools-devel

затем скачатьnginxизисходный код, используя только что полученные параметры компиляции, добавляем--with-http_auth_request_moduleперекомпилировать параметр

# ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --with-http_auth_request_module

# make

# make install

Сноваnginx -VПроверьте это, у вас есть этоhttp_auth_request_moduleохватывать

# nginx -V
nginx version: nginx/1.14.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC) 
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --with-http_auth_request_module

простая демонстрация

nginx.incДана очень простая демонстрация, а именноnginx-auth-ldapэто репо.

Вся логика показана на следующем рисунке

image.png

Подробная блок-схема примерно выглядит следующим образом:


image.png
  1. Клиент отправляет HTTP-запрос, чтобы получить защищенный ресурс обратного прокси на Nginx.

  2. Nginxauth_requestмодуль перенаправляет запрос наldap-authЭтот сервис (соответствующий nginx-ldap-auth-daemon.py) обязательно выдаст ошибку 401 .

  3. Nginx перенаправляет запрос наhttp:// backend / login, который здесь соответствует серверной службе. будетuriнаписатьX-Target, так что вы можете прыгать позже.

  4. Серверная служба отправляет клиенту форму входа (формаdemoопределено в кодексе). в соответствии сerror_pageконфигурации, Nginx вернет код статуса http формы входа как 200.

  5. Пользователь заполняет поля имени пользователя и пароля в форме и нажимает кнопку входа в систему из/ loginположить началоPOSTЗапрос, Nginx перенаправляет его на серверную службу.

  6. Серверная служба записывает имя пользователя и пароль в файл cookie в формате base64.

  7. Клиент повторно отправляет свой первоначальный запрос (из шага 1), теперь уже с файлом cookie. Nginx перенаправляет запрос наldap-authслужбы (как показано на шаге 2).

  8. ldap-authСлужба декодирует файл cookie, а затем выполняет аутентификацию LDAP.

  9. Дальнейшее действие зависит от успеха аутентификации LDAP:

    • Если аутентификация прошла успешно, тоldap-authСлужба возвращает код состояния 200 в Nginx. Nginx запрашивает ресурсы у серверных служб. существуетdemo, серверная служба возвращает следующий текст:
         Hello, world! Requested URL: URL
      
    • Если аутентификация не удалась,ldap-authСлужба возвращает 401 . Nginx снова перенаправляет запрос на серверную службу.Login(как в шаге 3) и повторите процесс.

Демонстрационный тест

Сначала установите зависимости

yum install python-ldap

Затем клонируйте репо

#git clone https://github.com/nginxinc/nginx-ldap-auth.git
# ls
backend-sample-app.py  Dockerfile  nginx-ldap-auth.conf              nginx-ldap-auth-daemon-ctl.sh  nginx-ldap-auth.default    nginx-ldap-auth.service  rpm
debian                 LICENSE     nginx-ldap-auth-daemon-ctl-rh.sh  nginx-ldap-auth-daemon.py      nginx-ldap-auth.logrotate  README.md

некоторые изnginx-ldap-auth.confЭто пример конфигурации Nginx, просто скопируйте его напрямую

# cp nginx-ldap-auth.conf /etc/nginx/nginx.conf

Конфигурационный файл Nginx выглядит следующим образом, он упрощен и добавлены китайские комментарии.

error_log logs/error.log debug;
# 这里把日志放在 nginx 目录下,所以要么改掉要么在 nginx 目录下建个 log 目录
events { }

http {
    # cache 路径和大小
    proxy_cache_path cache/  keys_zone=auth_cache:10m;

    # 将要被 nginx auth_request 保护的 backend 
    # 在这个 demo 里是 backend-sample-app.py.
    upstream backend {
        server 127.0.0.1:9000;
    }

    # nginx 服务起在 8081 上
    server {
        listen 8081;

        # 这个路径被 auth_request 保护了,  401 重定向到 login 上
        location / {
            auth_request /auth-proxy;

            # redirect 401 to login form
            error_page 401 =200 /login;

            proxy_pass http://backend/;
        }
        # 这里是我们认证的页面
        location /login {
            proxy_pass http://backend/login;
            # 这个 X-Target 是给认证完以后重定向的
            proxy_set_header X-Target $request_uri;
        }
        # 这是用做 auth_request 请求的路径
        location = /auth-proxy {
            internal;
            # 提供 ldap 认证服务的 auth-proxy backend
            # 这个 demo 里是 nginx-ldap-auth-daemon.py.
            proxy_pass http://127.0.0.1:8888;

            proxy_pass_request_body off;
            proxy_set_header Content-Length "";
            proxy_cache auth_cache;
            proxy_cache_valid 200 10m;

            # cookie 会加在这里
            proxy_cache_key "$http_authorization$cookie_nginxauth";

            # ldap 的地址
            proxy_set_header X-Ldap-URL      "ldap://ldap.example.org";

            # 是否开启 starttls
            # 注意 starttls 不能和 tls,也就是 ldaps 同时开启
            #proxy_set_header X-Ldap-Starttls "true";

            # ldap 的  BaseDN
            proxy_set_header X-Ldap-BaseDN   "dc=example,dc=org";

            # ldap 的 binddn,也就是有查询权限的账号
            proxy_set_header X-Ldap-BindDN   "cn=manager,dc=example,dc=org";

            # binddn 的密码
            proxy_set_header X-Ldap-BindPass "password";

            # cookie 的名字和值
            proxy_set_header X-CookieName "nginxauth";
            proxy_set_header Cookie nginxauth=$cookie_nginxauth;

            # ldap 的 searchFilter,就是拿哪个字段作为认证的用户名
            proxy_set_header X-Ldap-Template "(uid=%(username)s)";
        }
    }
}

а затем выполнить./nginx-ldap-auth-daemon.pyи./backend-sample-app.pyВот и все.

Посетите порт Nginx 8081, вы можете увидеть, что он может перенаправить наbackendИдите на сертификацию.

image.png image.png

бревно

# ./nginx-ldap-auth-daemon.py 
Start listening on localhost:8888...
localhost.localdomain - - [06/Jun/2018 09:18:31] using username/password from authorization header
localhost.localdomain - - [06/Jun/2018 09:18:31] "GET /auth-proxy HTTP/1.0" 401 -
localhost.localdomain - - [06/Jun/2018 09:18:35] using username/password from authorization header
localhost.localdomain - - [06/Jun/2018 09:18:35] "GET /auth-proxy HTTP/1.0" 401 -
localhost.localdomain - - [06/Jun/2018 09:18:43] using username/password from cookie nginxauth
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] searching on server "ldap://202.120.83.219" with base dn "dc=ecnu,dc=edu,dc=cn" with filter "(uid=20150073)"
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] Auth OK for user "20150073"
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] "GET /auth-proxy HTTP/1.0" 200 -
# ./backend-sample-app.py 
localhost.localdomain - - [06/Jun/2018 09:18:31] "GET /login HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:35] "GET /login HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "POST /login HTTP/1.0" 302 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "GET / HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "GET /favicon.ico HTTP/1.0" 200 -

анализ кода

весьdemoКромеpython-ldapДругих зависимостей нет. этоhttpСервис используетHTTPServerмодуль.

Первый взглядbackend-sample-app.py, это нашdemoвнутреннийbackend.

Во-первых, это маршрутизация:

    def do_GET(self):

        url = urlparse.urlparse(self.path)

        if url.path.startswith("/login"):
            return self.auth_form()

        self.send_response(200)
        self.end_headers()
        self.wfile.write('Hello, world! Requested URL: ' + self.path + '\n')

Как видите, он поддерживает два маршрута. просить/loginПросто пропустите страницу аутентификации, иначе выведитеHello, world!

Тогда посмотрите на эту форму:

    # send login form html
    def auth_form(self, target = None):

        # try to get target location from header
        if target == None:
            target = self.headers.get('X-Target')

        # form cannot be generated if target is unknown
        if target == None:
            self.log_error('target url is not passed')
            self.send_response(500)
            return

        html="""
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv=Content-Type content="text/html;charset=UTF-8">
    <title>Auth form example</title>
  </head>
  <body>
    <form action="/login" method="post">
      <table>
        <tr>
          <td>Username: <input type="text" name="username"/></td>
        <tr>
          <td>Password: <input type="text" name="password"/></td>
        <tr>
          <td><input type="submit" value="Login"></td>
      </table>
        <input type="hidden" name="target" value="TARGET">
    </form>
  </body>
</html>"""

        self.send_response(200)
        self.end_headers()
        self.wfile.write(html.replace('TARGET', target))

Сама форма аутентификации очень проста, вы можете видеть, что она помещает заголовок http вX-TargetВыньте его и повторно отправьте из формы скрытым способом. этоX-TargetПоле будет использоваться для перенаправления обратно на исходную страницу запроса после аутентификации.

Посмотрите, что делать после получения отправленных данных:

        user = form.getvalue('username')
        passwd = form.getvalue('password')
        target = form.getvalue('target')

        if user != None and passwd != None and target != None:

            # form is filled, set the cookie and redirect to target
            # so that auth daemon will be able to use information from cookie

            self.send_response(302)

            # WARNING WARNING WARNING
            #
            # base64 is just an example method that allows to pack data into
            # a cookie. You definitely want to perform some encryption here
            # and share a key with auth daemon that extracts this information
            #
            # WARNING WARNING WARNING
            enc = base64.b64encode(user + ':' + passwd)
            self.send_header('Set-Cookie', 'nginxauth=' + enc + '; httponly')

            self.send_header('Location', target)
            self.end_headers()

            return

Напишите здесь cookie, введите имя пользователя и пароль с помощью;После подключения выполните base64 для записи cookie. (Обратите внимание, что это очень небезопасно! Не пишите это в рабочей среде). затем написатьLocationнаписать вtargetзначение для реализации перенаправления и возврата.
Это грубо, в конце концов, это всего лишь демонстрация.

тогда мы видимnginx-ldap-auth-daemon.py. Это отвечает за nginxauth_requestответ на.

Нет файла cookie или неверный файл cookie, возврат 401:

        auth_header = self.headers.get('Authorization')
        auth_cookie = self.get_cookie(ctx['cookiename'])

        if auth_cookie != None and auth_cookie != '':
            auth_header = "Basic " + auth_cookie
            self.log_message("using username/password from cookie %s" %
                             ctx['cookiename'])
        else:
            self.log_message("using username/password from authorization header")

        if auth_header is None or not auth_header.lower().startswith('basic '):

            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm="' + ctx['realm'] + '"')
            self.send_header('Cache-Control', 'no-cache')
            self.end_headers()

Файл cookie правильный, base64 декодирует имя пользователя и пароль в:

        try:
            auth_decoded = base64.b64decode(auth_header[6:])
            user, passwd = auth_decoded.split(':', 1)

        except:
            self.auth_failed(ctx)
            return True

        ctx['user'] = user
        ctx['pass'] = passwd

Затем возьмите имя пользователя и пароль, а также информацию о конфигурации ldap в заголовке http, чтобы выполнить аутентификацию ldap. Эта часть кода довольно длинная, поэтому возьмем короткий фрагмент:

            ldap_obj = ldap.initialize(ctx['url']);

            # Python-ldap module documentation advises to always
            # explicitely set the LDAP version to use after running
            # initialize() and recommends using LDAPv3. (LDAPv2 is
            # deprecated since 2003 as per RFC3494)
            #
            # Also, the STARTTLS extension requires the
            # use of LDAPv3 (RFC2830).
            ldap_obj.protocol_version=ldap.VERSION3

            # Establish a STARTTLS connection if required by the
            # headers.
            if ctx['starttls'] == 'true':
                ldap_obj.start_tls_s()

            # See http://www.python-ldap.org/faq.shtml
            # uncomment, if required
            # ldap_obj.set_option(ldap.OPT_REFERRALS, 0)

            ctx['action'] = 'binding as search user'
            ldap_obj.bind_s(ctx['binddn'], ctx['bindpasswd'], ldap.AUTH_SIMPLE)

            ctx['action'] = 'preparing search filter'
            searchfilter = ctx['template'] % { 'username': ctx['user'] }

            self.log_message(('searching on server "%s" with base dn ' + \
                              '"%s" with filter "%s"') %
                              (ctx['url'], ctx['basedn'], searchfilter))

            ctx['action'] = 'running search query'
            results = ldap_obj.search_s(ctx['basedn'], ldap.SCOPE_SUBTREE,
                                          searchfilter, ['objectclass'], 1)

            ctx['action'] = 'verifying search query results'
            if len(results) < 1:
                self.auth_failed(ctx, 'no objects found')
                return

            ctx['action'] = 'binding as an existing user'
            ldap_dn = results[0][0]
            ctx['action'] += ' "%s"' % ldap_dn
            ldap_obj.bind_s(ldap_dn, ctx['pass'], ldap.AUTH_SIMPLE)

            self.log_message('Auth OK for user "%s"' % (ctx['user']))

            # Successfully authenticated user
            self.send_response(200)

В основном это стандартный процесс аутентификации ldap.

  1. братьbinddnиbindpasswdСначала выполните привязку, чтобы получить разрешение на запрос.
  2. взять имя пользователя иsearchFilterзапросить и получитьdn.
  3. возьми этоdnЗатем выполните привязку пароля пользователя, чтобы выполнить проверку аутентификации ldap.

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

Код этой демонстрации, безусловно, не может быть непосредственно применен в производственной среде.HTTPServerПодходит ли это для производственной среды или нет, я боюсь, что запись имени пользователя и пароля непосредственно в файл cookie недопустима. Даже совместное использование ключа для шифрования, такого как AES, как упоминалось в комментариях, не очень удобно.

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

И нам также нужно наложить некоторые специальные политики, например, некоторые IP-адреса выпускаются напрямую без аутентификации, и выбрать, следует ли включать аутентификацию в соответствии с периодом времени. Все это требует сеансов для поддержки.

Давайте сохраним эту часть для следующего раза.

использованная литература

nginx-ldap-auth
nginx-plus-authenticate-users

выше

Разрешение на перепечатку

CC BY-SA