Files
Test1/docs/architecture.md
T

225 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура решения: портал администрирования 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`:
```bash
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`),
но код валидации на стороне внутренних систем — задача их владельцев.