🛡 Модель безопасности приложения
Как платформа защищает деньги, данные и доступ на уровне кода. Инфра-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 / многоуровневое одобрение
- Платежи выше
Tariff.dual_approval_threshold(пер-валюта) требуют 2 РАЗНЫХ операторов:PaymentApprovalс unique(order_id, operator_id)→ один оператор считается один раз, второй голос → исполнение. - AML-кейсы -
required_approval_level1/2/3 (low/medium/high), голоса вAmlApprovalс unique(case_id, operator_id). Высокий риск (страна/санкции) при онбординге авто-заводит кейс и блокирует активацию до закрытия.
Карты и 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).
Секреты
- Нет секретов в коде и в git - только через env/
.envна сервере (chmod 600), впрыск оператором мимо чата. Целевое - sops+age (конфиги в git зашифрованы) + Docker secrets в рантайме. - В БД пароли - bcrypt (не логируются). Значения токенов/ключей не печатаются в логи.
Аудит
Действия операторов пишутся в 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 вместо статуса.