17 KiB
Архитектура решения: портал администрирования HAProxy + Django + AD
Вариант развёртывания: одна ВМ, без контейнеризации
1. Назначение
Единая точка входа для администраторов инфраструктуры: пользователь логинится
своей доменной учётной записью, видит только те административные ресурсы,
которые открыты его AD-группе, и одним кликом переходит в конкретную
систему по адресу https://<FQDN>/<adminusergroup>/<resource>/..., не
запоминая адреса и не имея прямого доступа к внутренним админкам, минуя
портал.
Этот документ описывает вариант развёртывания без Docker — все
компоненты ставятся как обычные пакеты дистрибутива и управляются через
systemd на одной виртуальной машине. Это упрощает эксплуатацию в
организациях, где контейнеризация не принята, и снимает зависимость от
Docker-инфраструктуры.
2. Компоненты и то, как они запущены на ВМ
| Компонент | Как установлен | Как запущен | Слушает |
|---|---|---|---|
| HAProxy | пакет haproxy из репозитория дистрибутива |
systemd-юнит haproxy.service (штатный, из пакета) |
0.0.0.0:80, 0.0.0.0:443, 127.0.0.1:8404 (статистика) |
| Django frontend (portal) | Python venv в /opt/admin-portal/venv, код в /opt/admin-portal/django_app |
Gunicorn под собственным systemd-юнитом admin-portal.service, от имени системного пользователя adminportal |
127.0.0.1:8000 |
| Active Directory | внешняя система, ничего не устанавливаем | — | LDAP/LDAPS (389/636) на контроллере домена |
| PostgreSQL | пакет postgresql |
systemd-юнит postgresql.service (штатный) |
127.0.0.1:5432 |
| Redis | пакет redis-server |
systemd-юнит redis-server.service (штатный) |
127.0.0.1:6379 |
| Backend admin panels | в решении — 2 демо-заглушки на Nginx; в проде — реальные системы | демо: nginx.service, отдельные server-блоки на 127.0.0.1:8081/8082; прод: на других хостах сети |
только внутри периметра, не публикуются наружу |
Все процессы, кроме HAProxy, слушают исключительно 127.0.0.1 — наружу
из ВМ открыты только порты 80/443 (см. ufw в README, шаг 11). Это заменяет
собой изоляцию, которую в контейнерном варианте давала Docker-сеть.
3. Общая схема
HTTPS
Браузер администратора ────────────────► HAProxy :443 (systemd: haproxy)
│
TLS termination, ACL-роутинг по path, HSTS, rate-limit
│
┌──────────────────────────────┼───────────────────────────────┐
│ путь НЕ ресурсный, │ путь ресурсный + нет │ путь ресурсный + есть
│ или /login /static/... │ resource_token │ resource_token
▼ ▼ ▼
┌─────────────────────────────────────────┐ ┌──────────────────────┐
│ Gunicorn 127.0.0.1:8000 (systemd: │ │ Nginx 127.0.0.1:808x │
│ admin-portal.service) │ │ (демо) или реальная │
│ - django-auth-ldap -> AD bind │ │ админка на др. хосте │
│ - каталог ресурсов из PostgreSQL │ └──────────────────────┘
│ - сессии в Redis │ ▲
│ - выпуск JWT для перехода на ресурс │──── redirect + cookie ───┘
└─────────────────────────────────────────┘ resource_token
│
▼
PostgreSQL 127.0.0.1:5432 (ResourceGroup, Resource, AccessLog)
Redis 127.0.0.1:6379 (сессии Django, кэш групп LDAP)
4. Поток аутентификации
- Браузер отправляет запрос на
https://fqdn/. - HAProxy (см.
haproxy.cfg) не находит cookieresource_tokenна нересурсном пути → направляет наbackend be_django(Gunicorn,127.0.0.1:8000). - Django рендерит форму логина. Пользователь вводит
sAMAccountName+ пароль. django-auth-ldap:- выполняет bind сервисным аккаунтом (
AUTH_LDAP_BIND_DNиз/etc/admin-portal/admin-portal.env), - ищет пользователя по
AUTH_LDAP_USER_SEARCH(sAMAccountName=%(user)s), - выполняет повторный bind уже от имени найденного DN с введённым паролем — это и есть проверка пароля,
- при успехе подтягивает атрибуты (
givenName,sn,mail) и список групп (AUTH_LDAP_GROUP_SEARCH), кладёт их вrequest.user.ldap_user.
- выполняет bind сервисным аккаунтом (
- Django создаёт сессию в Redis (локальный,
127.0.0.1:6379), выставляет cookiesessionid. - Пользователь видит дашборд со списком
Resource, чьиResourceGroup.ad_group_nameвходят в набор групп пользователя из AD.
5. Поток перехода на ресурс
- Клик по карточке ресурса →
GET /{group}/{resource}/. - Первый переход всегда обрабатывается Django (
open_resourceview), поскольку у HAProxy ещё нет cookieresource_tokenдля этого пути — маршрутis_resource_path !has_resource_tokenвhaproxy.cfgуводит запрос наbe_django. open_resourceview:- проверяет, что ресурс существует, активен и открыт AD-группе пользователя,
- пишет запись в
AccessLog(кто, когда, какой ресурс, IP — таблица в PostgreSQL), - выпускает JWT (
RESOURCE_TOKEN_SECRET, TTL по умолчанию 60 секунд), - кладёт токен в cookie
resource_token, ограниченнуюpathконкретного ресурса, - делает 302 редирект на тот же путь.
- HAProxy на повторном запросе видит
cook(resource_token) -m found→ маршрутизация поgroup_map.cfgна нужныйbackend(демо-Nginx на127.0.0.1:808xили реальная система на другом хосте). Перед проксированием HAProxy снимает cookie в заголовокX-Resource-Tokenи удаляет исходныйCookie— backend никогда не видитsessionidпортала. - Backend admin panel (при наличии соответствующей интеграции) валидирует
JWT тем же
RESOURCE_TOKEN_SECRETи на основанииsubлогинит/находит пользователя в своей системе.
Важно: токен — это разовый хэндшейк, а не сессия внутри backend'а
resource_token живёт всего 60 секунд и привязан к path конкретного
ресурса. Backend admin panel, приняв и провалидировав токен один раз,
должен завести собственную сессию для дальнейшей работы пользователя — так
делает подавляющее большинство систем при интеграции с внешним SSO. Если
полагаться на resource_token при каждом запросе, через 60 секунд любой
запрос без свежего токена уйдёт обратно в Django на переоформление. При
необходимости TTL можно увеличить через RESOURCE_TOKEN_TTL_SECONDS в
.env, но это не заменяет полноценную сессию backend'а.
6. Варианты передачи identity в backend (сравнение)
| Вариант | Как работает | Когда применять |
|---|---|---|
A. Доверенный заголовок (X-Remote-User) |
HAProxy/Django прокидывает имя пользователя как есть, backend доверяет заголовку | Полностью изолированный сегмент сети, backend не имеет других входов |
| B. Подписанный JWT (реализовано в этом решении) | Django выпускает короткоживущий токен, backend проверяет подпись | Рекомендуемый вариант по умолчанию |
| C. Полноценный SSO (SAML/OIDC) | Backend сам является Service Provider, IdP — AD FS/Keycloak | Backend уже умеет SAML/OIDC "из коробки" (Grafana, GitLab и т.п.) |
7. Модель данных
ResourceGroup
ad_group_name (slug, unique) -- реальное имя группы AD, сегмент пути
display_name (str)
Resource
group -> ResourceGroup
slug (str) -- второй сегмент пути
name (str)
description (text)
is_active (bool)
unique(group, slug)
AccessLog
user -> auth.User (nullable)
resource -> Resource (nullable)
ip_address (inet)
created_at (datetime, auto)
Хранится в PostgreSQL, установленном локально на ВМ (/var/lib/postgresql).
8. Наполнение и сопровождение каталога ресурсов
- Django admin (
https://<FQDN>/django-admin/) — для точечных правок. - Декларативный YAML + management-команда
sync_resources:Команда идемпотентна, можно хранить YAML в git и гонять через CI/CD по SSH.sudo -u adminportal bash -c \ 'set -a; source /etc/admin-portal/admin-portal.env; set +a; \ /opt/admin-portal/venv/bin/python /opt/admin-portal/django_app/manage.py \ sync_resources /opt/admin-portal/django_app/resources.example.yaml'
9. Безопасность
- TLS 1.2+/1.3, HSTS, современные cipher suites — терминируются на HAProxy;
все внутренние компоненты общаются по
127.0.0.1в открытом виде (трафик не покидает ВМ) либо по внутренней сети без выхода наружу. - Пароли пользователей не хранятся на портале — только bind-запрос к AD.
- CSRF-защита на всех формах Django.
- Сессионные cookie:
Secure,HttpOnly,SameSite=Lax. resource_token— отдельный секрет, TTL 60 секунд,path-scope,HttpOnly.- HAProxy снимает cookie сессии портала перед проксированием на backend.
- Аудит: каждый переход на ресурс логируется в
AccessLog. - Rate-limit на
/login/черезstick-tableв HAProxy. - Файл
/etc/admin-portal/admin-portal.envс секретами — права640, владелецroot:adminportal, читать может только процесс приложения. - systemd-юнит
admin-portal.serviceзапущен от непривилегированного пользователяadminportal(nologin), сNoNewPrivileges,PrivateTmp,ProtectSystem=full. ufwоткрывает наружу только 80/443; PostgreSQL, Redis, Gunicorn и демо-Nginx слушают исключительно loopback — недоступны из сети в принципе, даже без дополнительных правил фаервола.- Backend admin panels в проде должны быть в отдельном сетевом сегменте без прямого доступа из интернета — HAProxy как единственная точка входа снаружи.
10. Отказоустойчивость и масштабирование без Docker
Однонодовый вариант (этот пакет) подходит для старта и для организаций среднего размера. Пути роста при увеличении нагрузки:
- HAProxy: поднять второй экземпляр HAProxy на второй ВМ +
keepalived(VRRP) для плавающего IP, либо использовать внешний L4-балансировщик перед парой ВМ с HAProxy. - Django/Gunicorn: поскольку сессии и кэш групп уже вынесены в Redis,
можно поднять
admin-portal.serviceна второй ВМ тем же способом (шаги 3–8 из README) и добавить вbackend be_djangoвторую строкуserver django2 10.0.0.11:8000 check— балансировка без изменения остальной конфигурации. - PostgreSQL: для прод-контура — вынести на отдельную ВМ с репликой
(потоковая репликация PostgreSQL), поменять
POSTGRES_HOSTв.envна обеих Django-нодах. - Redis: при росте нагрузки — Redis Sentinel (2–3 ВМ), либо вынос на отдельную ВМ без кластеризации, если нагрузка умеренная (сессии — лёгкая нагрузка для Redis).
- LDAP: кэш групп на
AUTH_LDAP_CACHE_TIMEOUT(по умолчанию 600 секунд) снижает нагрузку на контроллеры домена независимо от количества Django-нод.
11. Ограничения и что осознанно вынесено за рамки
- Установочный скрипт (
scripts/install.sh) рассчитан на Ubuntu/Debian (apt); для RHEL/CentOS/Alma потребуется адаптировать имена пакетов (dnf install haproxy nginx postgresql-server redis python3 ...) и путь доpython3-venv— сама архитектура и код Django не меняются. - TLS-сертификат от внешнего центра сертификации подкладывается вручную —
автоматический выпуск/продление (например, через
certbot) в рамках данного пакета не настраивается, так как процедура сильно зависит от того, публичный это FQDN или внутренний корпоративный CA. - Мультиузловая отказоустойчивость (второй HAProxy, реплика PostgreSQL, Redis Sentinel) описана на уровне принципов в разделе 10, но конкретные Ansible/Terraform-плейбуки под неё не входят в этот пакет — количество ВМ и топология сильно зависят от инфраструктуры конкретной организации.
- Интеграция конкретных backend admin panel с валидацией JWT специфична для
каждой системы; в решении заложен универсальный контракт (заголовок
X-Resource-Token, алгоритмHS256, общий секретRESOURCE_TOKEN_SECRET), но код валидации на стороне внутренних систем — задача их владельцев.