Skip to content

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)Маршруты
marketplacepartizap.rudev.partizap.ru/store/*, /vendor/*, /auth/*, /health
adminadmin.partizap.rudev-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_log FK переключен с 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)

МеханизмMarketplaceAdmin
Lockout10/30min3/30min
Progressive delays4→2s, 6→5s, 8→10s2→2s, 3→5s
Alert email>15 попытокКаждая попытка
TOTPНетОбязательно
Rate limit (login)5/900s3/1800s
Rate limit (global)100/60s30/60s

PostgreSQL column-level grants

Три роли: partizap_owner (DDL), partizap_marketplace (runtime), partizap_admin (runtime).

Admin role имеет минимальные привилегии:

  • users: только SELECT + UPDATE(is_active) — не может менять password_hash
  • products: только SELECT + UPDATE(status, published_at, rejection_reason) + DELETE
  • admin_audit_log: только SELECT + INSERT — immutable audit trail
  • admin_users: SELECT + INSERT + UPDATE, без DELETE

Параметризация middleware

AuthMiddleware и SessionManager — один класс, поведение определяется через DI:

ПараметрMarketplaceAdmin
sessionKeyuser_idadmin_user_id
cookie namePARTIZAP_SESSIONPARTIZAP_ADMIN_SESSION
Redis DB04
SameSiteLaxStrict

Redis DB layout

DBНазначение
0Marketplace sessions + Doctrine metadata cache
1Application cache (categories, car_makes, geo)
2Queues
3Rate limits
4Admin 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/core composer-пакет (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