Безопасность довёл до ума — Cursor-генерацию переписал руками. IDOR закрыл, CSRF задушил, refresh rotation теперь как надо. HSTS на staging, ENABLE_DOCS=false, install.env recovery протестил. Backend: - jwt_denylist + auth_epoch: мгновенный revoke access JWT (logout/block/reset) - auth/admin/users: bump epoch, logout с Bearer, forgot_password skip для blocked - install_secrets: путь всегда apps/api/data/secrets/ (bootstrap из корня не ломает Docker) - seed: SEED_DEMO_USERS=false на prod/staging - тесты: jwt revoke, integration, coverage gate 90% Frontend: - logout шлёт Bearer, обработка TOKEN_REVOKED - guards TypeScript fix - E2E: blocked user → 401 сразу после block Infra: - staging/prod compose, TLS nginx, deploy-скрипты - k6 §17.2, backup/health/smoke scripts Docs: - docs/ на русском: project, security, deploy, release (старые md слили) - README короткий + план ТЗ + стандартные логины dev Код готов к плаванию. Капитан может идти писать фронт.
5.1 KiB
5.1 KiB
Безопасность
Compton MVP — не банк, но и не «admin/admin в prod». Ниже — как устроена защита и что проверить перед выкладкой.
Auth: схема
sequenceDiagram
participant B as Браузер
participant API as FastAPI
participant RD as Redis
participant PG as PostgreSQL
B->>API: POST /auth/login
API->>PG: bcrypt verify
API->>RD: read auth_epoch
API-->>B: access JWT (memory) + refresh cookie
B->>API: GET /users/me + Bearer
API->>RD: jti denied? epoch ok?
alt revoked
API-->>B: 401 TOKEN_REVOKED
else ok
API-->>B: 200
end
B->>API: POST /auth/logout
API->>RD: deny_jti + revoke refresh
API-->>B: cookie cleared
Токены
| Токен | Где живёт | Отзыв |
|---|---|---|
| Access JWT | память frontend | jti denylist + auth_epoch (Redis) |
| Refresh | HttpOnly cookie | rotation + family reuse detection |
| Email/reset | opaque → hash в БД | one-time, TTL 1ч |
JWT claims: sub, role, jti, auth_epoch, exp.
Мгновенный revoke
apps/api/app/core/jwt_denylist.py:
| Redis key | Смысл |
|---|---|
jwt:deny:{jti} |
конкретный access-токен |
auth:epoch:{user_id} |
версия сессий пользователя |
| Событие | Действие |
|---|---|
| Logout | deny jti + revoke refresh |
| Block | INCR epoch + revoke refresh |
| Смена/reset пароля | INCR epoch + revoke refresh |
Production: без Redis API не стартует. Redis упал — fail-closed (401, не «ну ладно»).
Dev: in-memory fallback (не путать с prod).
Криптография
Всё через apps/api/app/core/crypto.py:
| Данные | Метод |
|---|---|
| Пароли | bcrypt cost 12 |
| Access JWT | HS256 |
| Refresh/email tokens | SHA-256 + pepper |
| Media URLs | HMAC-SHA256 + TTL |
| Install secrets | secrets.token_*, generate-once + lock |
Install secrets
python apps/api/scripts/bootstrap_install.py # до первого docker compose up
| Файл | Содержимое |
|---|---|
data/secrets/install.env |
PG, JWT, S3, MinIO |
install.meta.json |
install ID, lock time |
Не ротировать POSTGRES_PASSWORD / JWT после bootstrap без плана — иначе Postgres скажет фразу, которую вы уже видели, и будет прав.
Reveal: Admin → Security → Install Secrets (superuser, аудит в admin-audit.jsonl).
Восстановление: deploy.md § восстановление.
RBAC
| Правило | Enforcement |
|---|---|
/admin → role=admin |
Guard + API |
| Security/Diagnostics/secrets → superuser | API + UI tabs |
| Нельзя block/demote себя | admin service |
| Last admin protected | admin service |
| blocked/pending → 403 refresh | auth service |
| forgot_password skip для blocked | auth service |
| IDOR на профиль | users router |
HTTP / инфра
Nginx headers
| Header | Значение |
|---|---|
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| HSTS | default.tls.conf (staging/prod) |
Прочее
- Origin/Referer на cookie-auth endpoints
- Rate limit (prod: обязателен)
- CMS: bleach, протоколы http/https/mailto
- Avatar: jpeg/png/webp, re-encode, SVG — нет
- CI: Bandit, pip-audit, gitleaks, npm audit
- Postgres/Redis/MinIO — internal network
- Firewall VPS: 22, 80, 443
Production guards
APP_ENV=production → API не стартует, если:
| Проблема | Env |
|---|---|
| Test routes | ENABLE_TEST_ROUTES=true |
| OpenAPI | ENABLE_DOCS=true |
| Rate limit off | ENABLE_RATE_LIMIT=false |
| Cookie без Secure | COOKIE_SECURE=false |
| Placeholder JWT | change-me-* |
| Дефолтная БД | user:pass |
| Без SSL mode | нет sslmode=require |
| Без Redis | JWT revocation |
Prod env (минимум)
| Переменная | Значение |
|---|---|
APP_ENV |
production |
ENABLE_DOCS |
false |
ENABLE_TEST_ROUTES |
false |
COOKIE_SECURE |
true |
ENABLE_RATE_LIMIT |
true |
EMAIL_DELIVERY_MODE |
smtp |
SEED_DEMO_USERS |
false |
DATABASE_URL |
…?sslmode=require |
Чеклист
Сделано в коде
- JWT в memory, refresh HttpOnly
- Anti-enumeration auth
- Origin/Referer validation
- RBAC + superuser
- CMS sanitization
- Nginx security headers
- CI security scans
- Install secrets bootstrap + lock
- JWT jti denylist + auth_epoch
- Admin audit log
Проверить на staging
/api/v1/docs→ 404- HSTS за TLS
- Dry-run recovery секретов
- ZAP: 0 High/Critical → release.md
- k6 pass → release.md
- Lighthouse ≥ 85 на
/