Files
site/docs/security.md
T
влад 12c983c0fc Шхуна не тонет: security, infra и доки на русском.
Безопасность довёл до ума — 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

Код готов к плаванию. Капитан может идти писать фронт.
2026-07-15 00:06:13 +03:00

174 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Безопасность
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 на `/`