Patchman : Kerberos аутентификация и LDAP авторизация в Django в домене Active Directory

Patchman : Single Sign-On Authentication and Authorization in Django with Kerberos and LDAPРассмотренный ранее сервер Patchman на базе веб-фреймворка Django, в нашем случае функционирует в инфраструктуре, имеющей службу каталогов Microsoft Active Directory, поэтому для большего удобства работы с веб-интерфейсом Patchman возникает желание настроить на веб-сервере аутентификацию пользователей с помощью протокола Kerberos, а также авторизацию с помощью протокола LDAP через доменные группы безопасности. То есть, предполагается реализация механизма SSO (Single Sign-On) для того, чтобы пользователи не вводили какие-то локальные учётные записи каждый раз при входе в веб-интерфейс Patchman, а прозрачно получали доступ к сайту на основе своей доменной учётной записи, в контексте которой запущен клиентский браузер.Так как веб-интерфейс Patchman работает на базе веб-фреймворка Django, то, по сути, описываемая далее настройка будет относится именно к этому фреймворку и пример рассмотренной далее конфигурации может быть применим и для других проектов, реализованных на базе Django.

Всю настройку условно можно разделить на 3 части:

  • Настройка веб-сервера Apache для работы с аутентификацией Kerberos;
  • Настройка фреймворка Django на LDAP-авторизацию с помощью модуля django-auth-ldap;
  • Дополнительная конфигурация модуля django-remote-auth-ldap, чтобы "подружить" Kerberos аутентификацию с LDAP авторизацией.

 

Часть 1. Kerberos и Apache

В рамках данной заметки мы не будем рассматривать процедуры подключения сервера к домену или получения keytab-файла, необходимого для работы Kerberos в Linux. Предполагаем, что наш сервер Patchman уже подключен к домену, например так, как это описано в Вики-статье "Подключение Debian GNU/Linux 12 (Bookworm) к домену Active Directory с помощью SSSD и настройка PAM для доменной аутентификации и авторизации в SSHD". То есть в системе уже имеется keytab-файл, используемый для процессов доменной аутентификации Kerberos. В этом файле, как минимум, должны быть записи типа host/<server fqdn>.

Проверим содержимое записей keytab-файла:

# klist -e -k -t /etc/krb5.keytab

Регистрируем в домене для учётной записи веб-сервера SPN-запись вида HTTP/<server fqdn> и добавляем в keytab-файл соответствующие записи. Подробно эта процедура описана ранее в статье "Добавление SPN записей в keytab-файл (на стороне сервера Linux с помощью утилиты ktutil), связанный с учётной записью Computer в домене Active Directory".

Когда keytab-файл подготовлен, с помощью утилиты setfacl выставляем на него дополнительные разрешения, чтобы пользователь, от имени которого работает служба веб-сервера Apache, имел к этому файлу доступ на чтение:

# apt-get install acl
# setfacl -m u:www-data:r /etc/krb5.keytab

Проверяем результирующие права на файл:

# getfacl -p /etc/krb5.keytab
# file: /etc/krb5.keytab
# owner: root
# group: root
user::rw-
user:www-data:r--
group::---
mask::r--
other::---

Установим и активируем модуль GSSAPI для веб-сервера Apache:

# apt-get install libapache2-mod-auth-gssapi
# a2enmod auth_gssapi
# systemctl restart apache2

Настраиваем конфигурацию виртуальных каталогов веб-сервера:

# nano /etc/apache2/conf-enabled/patchman.conf

В самый конец секции описания каталога /patchman/reports/upload добавим правило, разрешающее доступ без аутентификации, но только после прохождения проверки правил "Require ip". Это нужно сделать, чтобы клиенты Patchman, отправляющие POST запрос с клиентским отчётом на данный URL не получали от веб-сервера требования на Kerberos аутентификацию. То есть данная секция примет примерно следующий вид:

<Location /patchman/reports/upload>
    # Add the IP addresses of your client networks/hosts here
    # to allow uploading of reports
    Require ip 192.168.1.0/255.255.255.0
    Require ip 127.0.0.0/255.0.0.0
    Require ip ::1/128
    Satisfy Any
</Location>

А в конец файла добавим секцию, определяющую настройки для всего каталога /patchman, где укажем, что требуется Kerberos аутентификация.

<Location /patchman>
    AuthType GSSAPI
    GssapiLocalName On
    GssapiCredStore keytab:/etc/krb5.keytab
    Require valid-user
</Location>

Перезапустим службу веб-сервера:

# systemctl restart apache2

Проверим доступ к сайту Patchman из браузера, запущенного в контексте доменного пользователя. Как минимум, мы не должны получать никаких ошибок веб-сервера и появления всплывающих окон браузера с запросом на ввод логина и пароля. При этом на клиентском компьютере в кешэ билетов Kerberos для текущего пользователя (в Windows проверяется командой klist) должен появится билет вида HTTP/<server fqdn> с именем нашего сервера Patchman.

 

Часть 2. Django и LDAP-авторизация с помощью модуля django-auth-ldap

В этой части мы настроим и проверим работу LDAP-авторизации при доступе пользователей к веб-страницам Patchman (пока безотносительно того, что выше мы настроили Kerberos).

Для возможности авторизации пользователей в Django с помощью протокола LDAP нам потребуется доустановить модуль django-auth-ldap. В онлайн-документации предлагается устанавливать этот модуль командой "pip install django-auth-ldap", но в стандартных репозиториях Debian 12 данный модуль есть в виде отдельного пакета, поэтому можем его установить так:

# apt-get install python3-django-auth-ldap

В качестве зависимостей к этому пакету подтянется ещё несколько дополнительных пакетов - python3-ldap, python3-pyasn1, python3-pyasn1-modules.

В нашем примере для доступа пользователей к веб-страницам Patchman будет использоваться простая двух-уровневая модель – группа администраторов и группа пользователей с уровнем прав "только на чтение". Для этих уровней доступа создадим в домене Active Directory две соответствующие локальные группы безопасности, например "Patchman-Admins" и "Patchman-Viewers".

Отредактируем основной конфигурационный файл Patchman:

# nano /etc/patchman/local_settings.py

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

По умолчанию в Django в составе Patchman настроен только один бакэнд аутентификации (ModelBackend), который использует встроенные учётные записи пользователей, хранящиеся в БД. В секции AUTHENTICATION_BACKENDS мы подключаем и делаем более приоритетным LDAPBackend. То есть, сначала авторизация будет проходить через LDAP, а в случае неудачи будет предпринята попытка использовать авторизацию в локальной базе пользователей. Если есть желание совсем отключить локальную авторизацию, то можно просто закомментировать строку с ModelBackend.

# Keep ModelBackend around for per-user permissions and maybe a local superuser.
AUTHENTICATION_BACKENDS = (
    "django_auth_ldap.backend.LDAPBackend",
    "django.contrib.auth.backends.ModelBackend",
)

Далее добавляем блок параметров необходимых для bind-а к LDAP каталогу. Обратите внимание на то, что я намеренно привожу некоторые закомментированные параметры, которые не нужны или не будут работать в нашем конкретном случае, но могут оказаться полезными в других инфраструктурных окружениях.

В параметре AUTH_LDAP_SERVER_URI определяем адрес LDAP сервера. В нашем случае это контроллеры домена AD. Использовать незащищённое подключение к LDAP серверу не рекомендуется, поэтому можно настроить защищённое соединение LDAPS (как в закомментированной строке) или StartTLS (как используется в нашем случае).

В параметре AUTH_LDAP_GLOBAL_OPTIONS определено, что мы доверяем любым сертификатам, которые предоставляет удалённый LDAP сервер для защиты соединения. Такая конфигурация несколько понижает уровень безопасности соединения, но избавляет нас от дополнительных манипуляций с необходимостью добавления сертификата LDAP сервера, а также сертификатов удостоверяющих центров сертификации, в перечень доверенных.

Параметры AUTH_LDAP_BIND_DN и AUTH_LDAP_BIND_PASSWORD определяют специальную сервисную учётную запись, от имени которой будет выполняться подключение к LDAP-серверу. В нашем случае эти параметры необходимы, так как анонимное подключение к LDAP-каталогу запрещено.

В том случае, если в LDAP-каталоге все учётные записи пользователей хранятся в одной структурной единице (OU), то можно попробовать улучшить данную конфигурацию включением опции AUTH_LDAP_BIND_AS_AUTHENTICATING_USER и указанием шаблона поиска учётной записи в опции AUTH_LDAP_USER_DN_TEMPLATE. В этом случае можно закомментировать/удалить опции AUTH_LDAP_BIND_DN и AUTH_LDAP_BIND_PASSWORD и не создавать отдельной сервисной учётной записи для подключения к LDAP-каталогу, так как подключение в этом случае будет выполняться от имени той учётной записи пользователя, который проходит аутентификацию. Однако, это будет иметь смысл лишь в том случае, если мы не планируем использовать SSO с Kerberos.

Параметр AUTH_LDAP_CACHE_TIMEOUT отвечает за время кеширования данных, полученных из LDAP-каталога, для сокращения объёма запросов к LDAP-серверам. Этот параметр имеет смысл использовать при условии большой активности пользователей при работе с веб-приложением.

# Baseline configuration.
#AUTH_LDAP_SERVER_URI = "ldaps://dc01.holding.com:636"
AUTH_LDAP_SERVER_URI = "ldap://dc01.holding.com,ldap://dc02.holding.com"
AUTH_LDAP_START_TLS = True
AUTH_LDAP_GLOBAL_OPTIONS = {ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER}
AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS: 0}
AUTH_LDAP_BIND_DN = "CN=Patchman-LDAP,OU=Service Accounts,DC=holding,DC=com"
AUTH_LDAP_BIND_PASSWORD = "mYp!zsw0rD"
#AUTH_LDAP_USER_DN_TEMPLATE = "CN=%(user)s,OU=staff,DC=example,DC=com"
#AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True
# Cache distinguished names and group memberships for an hour to minimize LDAP traffic.
#AUTH_LDAP_CACHE_TIMEOUT = 3600

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

import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion, LDAPGroupQuery, GroupOfNamesType

После этого добавляем блок с параметром AUTH_LDAP_GROUP_SEARCH, где определяется фильтр поиска доменных групп. Здесь должен быть указан OU, в котором мы храним наши группы доступа "Patchman-Admins" и "Patchman-Viewers".

# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    "OU=Security Groups,DC=holding,DC=com",
    ldap.SCOPE_SUBTREE,"(objectClass=group)",
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")

Затем добавляем блок с параметрами AUTH_LDAP_REQUIRE_GROUP и AUTH_LDAP_DENY_GROUP, где можем указать группы, для которых явно разрешён и явно запрещён доступ к веб-приложению. В закомментированных строках приведены примеры того, как может выглядеть данная конфигурация. В нашем примере используется только параметр AUTH_LDAP_REQUIRE_GROUP и в нём мы указываем обе группы доступа, в качестве обязательных, то есть если пользователь не входит ни в одну из этих групп, доступ ему будет запрещён.

# Simple group restrictions
#AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com"
#AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com"
# Or:
#from django_auth_ldap.config import LDAPGroupQuery
#AUTH_LDAP_REQUIRE_GROUP = (
#    LDAPGroupQuery("cn=enabled,ou=groups,dc=example,dc=com")
#    | LDAPGroupQuery("cn=also_enabled,ou=groups,dc=example,dc=com")
#) & ~LDAPGroupQuery("cn=disabled,ou=groups,dc=example,dc=com")
AUTH_LDAP_REQUIRE_GROUP = (
    LDAPGroupQuery("CN=Patchman-Viewers,OU=Security Groups,DC=holding,DC=com")
    | LDAPGroupQuery("CN=Patchman-Admins,OU=Security Groups,DC=holding,DC=com")
)

Далее добавляем блок с параметром AUTH_LDAP_USER_SEARCH с описанием всех OU, в которых следует искать учёную запись авторизуемого пользователя. Если все учётные записи пользователей хранятся в рамках одного конкретного OU, то согласно рекомендациям документа "Performance", вместо параметра AUTH_LDAP_USER_SEARCH можно использовать параметр AUTH_LDAP_USER_DN_TEMPLATE, который увеличивает скорость получения информации из LDAP-каталога и уменьшает количество запросов к этому каталогу. В нашем примере учётные записи пользователей веб-приложения хранятся в разных OU, поэтому мы перечисляем все возможные значения.

#AUTH_LDAP_USER_SEARCH = LDAPSearch(
#    "ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"
#)
# Or:
#AUTH_LDAP_USER_DN_TEMPLATE = 'sAMAccountName=%(user)s,ou=users,dc=example,dc=com'
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
    LDAPSearch("OU=Administrators,OU=Services,DC=holding,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
    LDAPSearch("OU=Operators,OU=Users,DC=holding,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
    LDAPSearch("OU=Testers,DC=holding,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
)

В случае успешной LDAP-авторизации учётная запись пользователя создаётся в локальной базе данных Django, копируя при этом атрибуты из LDAP-каталога. Следующий блок описывает правила сопоставления атрибутов создаваемого локального пользователя с атрибутами пользователя из Active Directory.

# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail",
}
AUTH_LDAP_ALWAYS_UPDATE_USER = True

В следующем важном блоке описываем правила сопоставления доменных групп пользователей с флагами is_active, is_staff, is_superuser, которые определяют уровень доступа к веб-приложению. Это те самые флаги, которые можно найти при просмотре свойств пользователя в административном интерфейсе Django:

Django permissions flags

Любой действующий пользователь в независимости от уровня доступа должен иметь включенный флаг is_active, поэтому в секции данного флага мы прописываем обе наши доменные группы безопасности. Пользователи с одним только включенным флагом is_active будут иметь самый ограниченный вид доступа в режиме "только на чтение" и не будут иметь доступа к веб-страницам администрирования Django. Флаги is_staff и is_superuser дают больше полномочий и определяют разные уровни доступа, но в рассматриваемом нами примере это разделение не важно, поэтому мы вписываем в эти секции этих флагов только группу административного доступа.

AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    "is_active": (
        LDAPGroupQuery("CN=Patchman-Viewers,OU=Security Groups,DC=holding,DC=com")
        | LDAPGroupQuery("CN=Patchman-Admins,OU=Security Groups,DC=holding,DC=com")
    ),
    "is_staff": "CN=Patchman-Admins,OU=Security Groups,DC=holding,DC=com",
    "is_superuser": "CN=Patchman-Admins,OU=Security Groups,DC=holding,DC=com",
}
# Use LDAP group membership to calculate group permissions.
AUTH_LDAP_FIND_GROUP_PERMS = True

Ну и, наконец, завершающий блок конфигурационного файла local_settings.py может содержать секцию LOGGING. Здесь могут быть подключены разные логгеры для целей отладки. В нашем примере описано подключение расширенного логирования для бакенда django_auth_ldap, что поможет диагностировать проблемы LDAP-авторизации в лог-файле веб-сервера. Разумеется, на отлаженной продуктивной системе такой блок лучше закомментировать.

# Enable debug for django_auth_ldap in /var/log/apache2/error.log
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {"django_auth_ldap": {"level": "DEBUG", "handlers": ["console"]}},
}

Сохраним изменения в файле local_settings.py и, учитывая то обстоятельство, что теперь этот конфигурационный файл содержит логин и пароль сервисной учётной записи для доступа к LDAP-каталогу, проведём корректировку прав доступа к этому файлу:

# chmod 640 /etc/patchman/local_settings.py

Выполним перезапуск службы веб-сервера:

# systemctl restart apache2

Теперь можем перейти на веб-страницу входа в веб-интерфейс Patchman и попытаться авторизоваться разными доменными пользователями, как входящими в группы безопасности "Patchman-Admins" и "Patchman-Viewers", так и не входящими в эти группы. Кроме того, локальная учётная запись администратора Patchman в нашей конфигурации также должна работать. Если все проверки пройдены успешно, то можем переходить к следующей части настройки с небольшой модификацией текущей конфигурации.

 

Часть 3. Конфигурация модуля django-remote-auth-ldap

Итак, на стороне веб-сервера у нас имеется уже работающая аутентификация Kerberos, а на стороне веб-фреймворка Django имеется работающая form-based аутентификация с авторизацией в LDAP каталоге. Но сейчас эти два механизма никак друг с другом не пересекаются. Нам потребуется пристроить к Django модуль django-remote-auth-ldap, который выстроит связь от аутентификации Kerberos к авторизации LDAP, позволив автоматически пропускать на веб-сайт Patchman пользователя успешно прошедшего аутентификацию (при условии его членства в соответствующей доменной группе безопасности).

На странице описания модуля  предлагается установка посредствам команды "pip install django-remote-auth-ldap". Однако в Debian 12, где модули python управляются путём установки deb-пакетов, выполнение такой команды приведёт к ошибке "error: externally-managed-environment".

На помощь в этой ситуации снова пришёл наш коллега Владимир Леттиев (aka crux) и опакетил модуль django-remote-auth-ldap под ОС Debian 12, за что ему "наше с кисточкой". Скачать готовый deb-пакет можно отсюда: python3-django-remote-auth-ldap

Размещаем пакет в локальном репозитории и привычным способом проводим его установку:

# apt-get install python3-django-remote-auth-ldap

Обратите внимание на то, что при установке пакета python3-django-remote-auth-ldap в качестве зависимости из стандартных репозиториев Debian 12 должен подтянуться ещё и пакет python3-django-appconf, который также необходим для корректной работы модуля django-remote-auth-ldap.

Внесём дополнительные коррективы в основной конфигурационный файл Patchman:

# nano /etc/patchman/local_settings.py

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

# Keep ModelBackend around for per-user permissions and maybe a local superuser.
AUTHENTICATION_BACKENDS = (
    "django_remote_auth_ldap.backend.RemoteUserLDAPBackend",
    #"django_auth_ldap.backend.LDAPBackend",
    "django.contrib.auth.backends.ModelBackend",
)

Под секцию описания бакендов добавим два дополнительных параметра, определяющих специфику передачи логина аутентифицированного в Kerberos пользователя к механизму авторизации в LDAP.

# django-remote-auth-ldap : whether or not to check the domain against a known list - default True
DRAL_CHECK_DOMAIN = False

# django-remote-auth-ldap : whether or not to strip the domain off the username before passing to django-auth-ldap - default True
DRAL_STRIP_DOMAIN = True

Кроме того, нам потребуется сделать "грешное", но необходимое, изменение в ещё в одном конфигурационном файле:

# nano /usr/lib/python3/dist-packages/patchman/settings.py

Здесь нужно найти секцию MIDDLEWARE и сразу после сроки с AuthenticationMiddleware добавить дополнительную строку с RemoteUserMiddleware:

MIDDLEWARE = [
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django_remote_auth_ldap.middleware.RemoteUserMiddleware',
    '...',
]

Опасность такого изменения заключается в том, что при последующих обновлениях пакета сервера Patchman (python3-patchman) файл settings.py может оказаться переписанным файлом, поставляемым в составе обновлённого пакета. В таком случае соответствующую правку файла придётся повторить.

На этом настройку можно считать законченной и можно приступать к окончательному тестированию построенной конфигурации. Если при работе с бакендом аутентификации django_remote_auth_ldap возникнут проблемы, то его отладку можно подключить через ранее упомянутый параметр конфигурации LOGGING, просто заменив в этой секции имя логгера django_auth_ldap на django_remote_auth_ldap.

Добавить комментарий