Безопасность довёл до ума — 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 Код готов к плаванию. Капитан может идти писать фронт.
174 lines
5.1 KiB
Markdown
174 lines
5.1 KiB
Markdown
# Безопасность
|
||
|
||
Compton MVP — не банк, но и не «admin/admin в prod». Ниже — как устроена защита и что проверить перед выкладкой.
|
||
|
||
## Auth: схема
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```bash
|
||
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 § восстановление](./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 |
|
||
|
||
## Чеклист
|
||
|
||
### Сделано в коде
|
||
|
||
- [x] JWT в memory, refresh HttpOnly
|
||
- [x] Anti-enumeration auth
|
||
- [x] Origin/Referer validation
|
||
- [x] RBAC + superuser
|
||
- [x] CMS sanitization
|
||
- [x] Nginx security headers
|
||
- [x] CI security scans
|
||
- [x] Install secrets bootstrap + lock
|
||
- [x] JWT jti denylist + auth_epoch
|
||
- [x] Admin audit log
|
||
|
||
### Проверить на staging
|
||
|
||
- [ ] `/api/v1/docs` → 404
|
||
- [ ] HSTS за TLS
|
||
- [ ] Dry-run recovery секретов
|
||
- [ ] ZAP: 0 High/Critical → [release.md](./release.md)
|
||
- [ ] k6 pass → [release.md](./release.md)
|
||
- [ ] Lighthouse ≥ 85 на `/`
|