Files
Test1/docs/architecture.md

17 KiB
Raw Permalink Blame History

Архитектура решения: портал администрирования 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. Поток аутентификации

  1. Браузер отправляет запрос на https://fqdn/.
  2. HAProxy (см. haproxy.cfg) не находит cookie resource_token на нересурсном пути → направляет на backend be_django (Gunicorn, 127.0.0.1:8000).
  3. Django рендерит форму логина. Пользователь вводит sAMAccountName + пароль.
  4. 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.
  5. Django создаёт сессию в Redis (локальный, 127.0.0.1:6379), выставляет cookie sessionid.
  6. Пользователь видит дашборд со списком Resource, чьи ResourceGroup.ad_group_name входят в набор групп пользователя из AD.

5. Поток перехода на ресурс

  1. Клик по карточке ресурса → GET /{group}/{resource}/.
  2. Первый переход всегда обрабатывается Django (open_resource view), поскольку у HAProxy ещё нет cookie resource_token для этого пути — маршрут is_resource_path !has_resource_token в haproxy.cfg уводит запрос на be_django.
  3. open_resource view:
    • проверяет, что ресурс существует, активен и открыт AD-группе пользователя,
    • пишет запись в AccessLog (кто, когда, какой ресурс, IP — таблица в PostgreSQL),
    • выпускает JWT (RESOURCE_TOKEN_SECRET, TTL по умолчанию 60 секунд),
    • кладёт токен в cookie resource_token, ограниченную path конкретного ресурса,
    • делает 302 редирект на тот же путь.
  4. HAProxy на повторном запросе видит cook(resource_token) -m found → маршрутизация по group_map.cfg на нужный backend (демо-Nginx на 127.0.0.1:808x или реальная система на другом хосте). Перед проксированием HAProxy снимает cookie в заголовок X-Resource-Token и удаляет исходный Cookie — backend никогда не видит sessionid портала.
  5. 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. Наполнение и сопровождение каталога ресурсов

  1. Django admin (https://<FQDN>/django-admin/) — для точечных правок.
  2. Декларативный YAML + management-команда sync_resources:
    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'
    
    Команда идемпотентна, можно хранить YAML в git и гонять через CI/CD по SSH.

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), но код валидации на стороне внутренних систем — задача их владельцев.