Безопасность
Раздел описывает, как «Оптима» защищает учётные записи, токены кабинетов WB и API. Решения зафиксированы в ADR: 0003, 0004, 0005, 0043, 0044, 0047.
Слои защиты запроса
Оба глобальных guard'а (ThrottlerGuard, затем GlobalJwtAuthGuard) подключены через
APP_GUARD: любой роут по умолчанию закрыт, открытые помечаются @Public() (fail-closed).
Аутентификация и сессии
| Аспект | Решение |
|---|---|
| Пароль | argon2id (64 MiB, timeCost 3, parallelism 1) — ADR-0003 |
| Access-токен | JWT HS256, живёт 15 минут (JWT_ACCESS_TTL); payload sub, email, role; проверяется по подписи без обращения к БД |
| Refresh-токен | непрозрачный случайный (32 байта), в БД лежит sha-256 хеш (refresh_tokens); TTL 30 дней (JWT_REFRESH_TTL) |
| Ротация | каждый /auth/refresh отзывает предъявленный токен и выдаёт новый |
| Семья токенов | family_id — одна сессия = одна цепочка ротаций (ADR-0043) |
| Reuse-detection | предъявление уже заменённого токена отзывает всю семью, пишется аудит auth.refresh-reuse-detected; клиент получает обезличенный 401 |
| Выход | POST /auth/logout (одна сессия), POST /auth/logout-all (все свои), POST /auth/users/:id/revoke-sessions (admin) |
| Локаут | после AUTH_MAX_FAILED_ATTEMPTS (10) неверных паролей вход блокируется на AUTH_LOCKOUT_MS (15 мин); одинаковый 401 и одинаковая латентность для «нет email / неверный пароль / блокировка» (фиктивный хеш выравнивает время) |
Окно компрометации access-токена — его TTL (15 мин): после смены роли или отзыва сессий уже выданный access действует до истечения, продлить его нельзя.
RBAC и доступ к данным
- Роли:
admin,seller(по умолчанию при регистрации),manager— Postgres-enumusers.role(ADR-0004). Роль читаетсяRolesGuardиз JWT, декоратор@Roles(...)на контроллерах. Матрица прав —backend/docs/rbac-matrix.md. - Scope по кабинету: сервисы работают от
actor(пользователь из JWT). Кабинет WB вне доступа пользователя отвечает404, а не403, — чтобы не подсказывать существование чужих кабинетов. Администратор видит все кабинеты. - Доступ к бренду: сущность
BrandAccess(модульbrand-access) выдаёт менеджеру доступ к бренду кабинета,BrandAccessGuardпроверяет его; выдача и отзыв — толькоadmin. - База знаний и RAG: видимость материалов (
visibility,status, гранты) применяется в SQL при поиске по векторам доORDER BY … LIMIT, плюс повторная проверка каждой строки выдачи (canAccessMaterial); фрагмент без владельца виден только администратору (fail-closed) — ADR-0047, см. ИИ-подсистема. - Первый администратор назначается вручную в БД (
UPDATE users SET role='admin'): через/auth/registerсоздаётся толькоseller.
Секреты и шифрование
- Токены кабинетов WB хранятся в
wb_accounts.api_key_encrypted; наружу отдаются только последние 4 символа (apiKeyLast4). Расшифровка — только в момент запроса к WB. - AES-256-GCM с key-ring (
core/crypto/EncryptionService): форматv1.{kid}.{iv}.{tag}.{ciphertext}, ключи —ENCRYPTION_KEYS=kid:hex64,…, активный —ENCRYPTION_ACTIVE_KID. Ротация без миграции данных: новые записи шифруются активным ключом, старые читаются прежним (ADR-0005). Ошибки расшифровки наружу уходят как обезличенный 500. - Fail-fast в production (ADR-0044): приложение не стартует, если активный ключ шифрования
или JWT-секреты — заглушки из репозитория (
change-me,placeholder), низкоэнтропийные, либо access- и refresh-секреты совпадают. - Секреты и пароли не попадают в логи (редактирование в структурных логах, см. Наблюдаемость); контент промптов LLM также не логируется.
Ограничение частоты и заголовки
| Механизм | Параметры (env, по умолчанию) |
|---|---|
| Общий лимит | THROTTLE_DEFAULT_LIMIT=300 за THROTTLE_DEFAULT_TTL_MS=60 000 |
Auth (/auth/login, /auth/register) | THROTTLE_AUTH_LIMIT=5 за 15 мин (THROTTLE_AUTH_TTL_MS=900 000) |
Refresh (/auth/refresh) | THROTTLE_REFRESH_LIMIT=60 за 1 мин (THROTTLE_REFRESH_TTL_MS=60 000) — вызывается при каждой загрузке страницы |
LLM-роуты (@ThrottleLlm()) | THROTTLE_LLM_LIMIT=30 за 1 час |
| Суточная квота LLM | 200 000 токенов на пользователя в сутки (константа LLM_DAILY_TOKEN_QUOTA), Redis op:llm:quota:{userId}:{day} |
Счётчики хранятся в Redis. За обратным прокси клиентский IP берётся из X-Forwarded-For с
доверием к TRUST_PROXY_HOPS прокси (по умолчанию 1).
- helmet: HSTS,
X-Content-Type-Options: nosniff,Referrer-Policy,X-Frame-Options: DENY; CSP отключён — API отдаёт JSON. - CORS: в dev/test открыт; в production — только origin'ы из
CORS_ORIGINS(через запятую), без переменной CORS-заголовки не выдаются. - Валидация входа: глобальный
ValidationPipeсwhitelistиforbidNonWhitelisted; лимит тела запроса — дефолт express (100 КБ).
Раскрытие информации в production
GET /healthотдаёт только{ status, timestamp }; детали проверок идут в лог (ADR-0044).- Swagger UI (
/api/docs) публичен во всех окружениях: он раскрывает только контракт (тот же, что вopenapi.json), а вызовы API из него требуют JWT. Bull Board (/admin/queues) в production закрыт HTTP Basic (QUEUE_BOARD_USER/PASSWORD);/metrics—METRICS_USER/PASSWORD(без них в production эндпоинт отвечает 404). - Compodoc собирается без исходного кода и приватных членов: портал документирует контракт, а не реализацию.
- Входящий
X-Request-Idпринимается только по маске[A-Za-z0-9._-]{1,128}, иначе генерируется новый (защита от инъекции в логи).
Аудит
Входы, отказы, блокировки, смена ролей, отзыв сессий, операции над кабинетами и действия ИИ-агента пишутся в журнал аудита — см. Наблюдаемость.
Переменные окружения — Переменные окружения; защита секретов в CI/CD — CI/CD.