🛡 Модель безопасности приложения

Как платформа защищает деньги, данные и доступ на уровне кода. Инфра-hardening серверов - в Архитектура Capital v1.0.md и runbooks/hardening-server.md. RBAC/сессии/CSRF - в 04 - RBAC и роли.md.

Изоляция клиента (мультитенантность кабинета)

Каждый запрос в кабинете проходит через get_current_company: активная компания обязана принадлежать залогиненному пользователю (проверка по Membership), иначе 401. Все выборки фильтруются по company.id в SQL - клиент физически не видит данных другого клиента. Это единая точка (linchpin) - изменения в ней проходят security-review отдельно.

Идемпотентность денежных путей

Деньги двигаются только через ledger-ядро (wallet.py). Каждая проводка несёт unique idempotency_key: повторный ключ не создаёт вторую запись (возвращается маркер replayed - вызывающий обязан пропустить побочные эффекты, напр. проводку выручки). Так двойной submit/повторный вебхук не удваивает движение денег.

Конкретные защиты: - Кошелёк - FOR UPDATE-лок перед любой мутацией баланса; debit проверяет available_balance ≥ amount под локом → нет TOCTOU на дневном лимите/балансе. - Комиссия за открытие счёта - Account лочится FOR UPDATE, долг читается под локом → сбор ровно один раз даже при гонке депозитов; RevenueEntry только при реальном движении денег (не phantom-revenue). - FX-конвертация идемпотентна по ref; реверс платежа защищён partial-unique (payment_order_id, source) на RevenueEntry. - Cap числа счетов на компанию - advisory-lock (защита check-then-act).

4-eyes / многоуровневое одобрение

Карты и PAN

🔒 Полный PAN никогда не персистится - в БД только pan_last4 + opaque issuer_card_id. Показ PAN: - Оператор: view-once + запись в CardRevealAudit (право cards.reveal). - Клиент: двухшаговый 3DS step-up (reveal → OTP эмитента → verify → показ один раз), rate-limit 5/мин, аудит только после успешного verify. Short-lived токен привязан к (client, card), TTL 5 мин, не реплеится и не переносится на другую карту.

Вебхуки - HMAC fail-closed

Все входящие вебхуки проверяют HMAC и закрыты по умолчанию (нет секрета/подписи → 401), идемпотентны: - SumSub POST /webhooks/sumsub - HMAC (алгоритм из заголовка), дедуп по кортежу события. - NS Cards POST /webhooks/nscards - HMAC-SHA256, дедуп по (card, issuer_tx_id).

Security-заголовки (SecurityHeadersMiddleware)

На обоих приложениях. Ставит: - Content-Security-Policy - разрешает inline-стили/скрипты (шаблоны используют inline) + Google Fonts (googleapis.com/gstatic.com), блокирует внешние скрипты/ объекты/фрейминг. - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=() - Strict-Transport-Security: max-age=31536000; includeSubDomains (HSTS 1 год)

robots.txt - Disallow all (в PUBLIC_PATHS).

Секреты

Аудит

Действия операторов пишутся в AuditLog (кто/что/над какой сущностью/когда). Целевое (см. арх-документ) - append-only + hash-chain (каждая запись несёт хэш предыдущей) + ежедневный WORM-дайджест в B2 с Object Lock → доказуемая неизменность.

Rate-limit / прокси

За Cloudflare-tunnel TRUST_PROXY=true → лимиты и IP-логика ключуются на CF-Connecting-IP. Логин-лимитер на онбординге и логине. Онбординг применяет IBAN-валидацию бенефициаров, size-cap + allowlist типов на загрузку документов (BLOB, sanitize имени, без path-traversal).

Graceful degradation при отказе партнёров

(Частично в коде, частично в плане - см. арх-документ, §7/§9.) Rail off → платёж локально/в очередь; SumSub упал → заявка сохраняется draft; Resend упал → email- очередь с ретраями. Клиент не видит 500 вместо статуса.