Шхуна не тонет: 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 Код готов к плаванию. Капитан может идти писать фронт.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# Безопасность
|
||||
|
||||
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 на `/`
|
||||
Reference in New Issue
Block a user