Перейти к основному содержимому

Безопасность

Раздел описывает, как «Оптима» защищает учётные записи, токены кабинетов 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-enum users.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 час
Суточная квота LLM200 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.