Appearance
ADR-010: Выделение админки в отдельный сервис
Дата: 2026-04-04
Статус: Принято
Авторы: Backend Team
Контекст
Админ-панель Partizap (36 action-классов, 36 маршрутов) была частью основного бэкенда маркетплейса. Это создавало проблемы:
- Единая точка входа — уязвимость в маркетплейсе открывает доступ к админским маршрутам
is_adminфлаг в таблицеusers— при компрометации БД атакующий видит, кто админ, и получает их password hash- Общий Redis — admin-сессии в том же keyspace, что и marketplace
- Невозможно применить отдельные security policies (mTLS, stricter rate limits, MFA)
- Маркетплейс-код загружается при каждом админском запросе
Решение
Один репозиторий, два деплоя (APP_MODE)
Переменная APP_MODE (marketplace | admin) определяет, какие маршруты и зависимости регистрируются:
| APP_MODE | Домен (prod) | Домен (dev) | Маршруты |
|---|---|---|---|
| marketplace | partizap.ru | dev.partizap.ru | /store/*, /vendor/*, /auth/*, /health |
| admin | admin.partizap.ru | dev-admin.partizap.ru | /admin/*, /admin/auth/*, /health, /store/* (read-only) |
Почему не отдельный репо: 80%+ кода — shared domain layer (entities, repositories, enums, services). Форк = дублирование и drift.
Отдельная таблица admin_users
is_adminфлаг удалён из таблицыusers- Админы хранятся в отдельной
admin_usersс собственными credentials - Маркетплейс DB role не имеет доступа к
admin_users,admin_sessions,admin_audit_log admin_audit_logFK переключен сusersнаadmin_users
Четыре уровня изоляции
| Уровень | Механизм |
|---|---|
| Сеть | mTLS — без клиентского сертификата nginx возвращает 400 |
| Приложение | APP_MODE, раздельные routes, параметризованные middleware |
| БД | PostgreSQL roles с column-level grants |
| Сессии | Redis DB 4, отдельный cookie (PARTIZAP_ADMIN_SESSION), SameSite=Strict |
mTLS (клиентские сертификаты)
Собственный CA, клиентские сертификаты для каждого админа. Без сертификата запрос не доходит до PHP — nginx отклоняет на уровне TLS handshake. CN сертификата логируется в audit log.
Почему не VPN: сотрудники используют коммерческий VPN (Red Shield). WireGuard создает VPN-over-VPN (double NAT, блокировка UDP). mTLS — обычный HTTPS-трафик, работает через любой VPN.
Аутентификация: три фактора
| Фактор | Тип | Уровень |
|---|---|---|
| Клиентский сертификат (.p12) | Устройство | TLS (Nginx) |
| Пароль admin_users | Знание | App (PHP) |
| TOTP-код | Устройство (телефон) | App (PHP) |
Компрометация одного фактора бесполезна:
- Украл пароль → нет сертификата → TLS handshake fail
- Украл сертификат → не знает пароль → 3 попытки → lockout → alert
- Украл cert + пароль → нет TOTP → 401
Brute-force защита (admin vs marketplace)
| Механизм | Marketplace | Admin |
|---|---|---|
| Lockout | 10/30min | 3/30min |
| Progressive delays | 4→2s, 6→5s, 8→10s | 2→2s, 3→5s |
| Alert email | >15 попыток | Каждая попытка |
| TOTP | Нет | Обязательно |
| Rate limit (login) | 5/900s | 3/1800s |
| Rate limit (global) | 100/60s | 30/60s |
PostgreSQL column-level grants
Три роли: partizap_owner (DDL), partizap_marketplace (runtime), partizap_admin (runtime).
Admin role имеет минимальные привилегии:
users: только SELECT + UPDATE(is_active) — не может менять password_hashproducts: только SELECT + UPDATE(status, published_at, rejection_reason) + DELETEadmin_audit_log: только SELECT + INSERT — immutable audit trailadmin_users: SELECT + INSERT + UPDATE, без DELETE
Параметризация middleware
AuthMiddleware и SessionManager — один класс, поведение определяется через DI:
| Параметр | Marketplace | Admin |
|---|---|---|
| sessionKey | user_id | admin_user_id |
| cookie name | PARTIZAP_SESSION | PARTIZAP_ADMIN_SESSION |
| Redis DB | 0 | 4 |
| SameSite | Lax | Strict |
Redis DB layout
| DB | Назначение |
|---|---|
| 0 | Marketplace sessions + Doctrine metadata cache |
| 1 | Application cache (categories, car_makes, geo) |
| 2 | Queues |
| 3 | Rate limits |
| 4 | Admin sessions |
Альтернативы
| Альтернатива | Почему отклонена |
|---|---|
| Отдельный git-репо (форк) | 80% shared code, maintenance burden |
| Docker-контейнер на сервис | Избыточно для текущего масштаба |
| nginx routing к одному процессу | Нет изоляции процессов и конфигурации |
| Отдельная БД для admin auth | Два Doctrine connection усложняют код |
| WireGuard VPN | Конфликт с Red Shield VPN (VPN-over-VPN) |
| Tailscale | Зависимость от SaaS |
| IP whitelist | Динамические IP у админов |
| is_admin флаг + APP_MODE | Компрометация БД раскрывает админов |
| Shared Redis для сессий | RCE в marketplace → lateral movement |
Фазы
- Phase 1 (реализовано): APP_MODE, admin_users, mTLS, TOTP, column-level grants, Redis isolation
- Phase 2 (будущее): Вынос shared domain в
partizap/corecomposer-пакет (path repository) - Phase 3 (будущее): Отдельные git-репо для admin и marketplace
Последствия
- 4 директории деплоя (prod/dev × marketplace/admin), каждая со своим
.env - Миграции запускаются только из marketplace-деплоя
- Nginx конфиги версионируются в отдельном git repo (
d_vyaznikov/nginx.git) - Каждый админ получает
.p12сертификат (перевыпуск раз в год) - При компрометации устройства — отзыв через CRL +
nginx reload - Три PostgreSQL роли вместо одной
- Deploy обновляет оба деплоя (marketplace + admin) одновременно
- Подробное руководство: Admin Service