Шхуна не тонет: 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:
влад
2026-07-15 00:06:13 +03:00
parent 86cc3fa541
commit 12c983c0fc
66 changed files with 2377 additions and 520 deletions
+41
View File
@@ -0,0 +1,41 @@
# Документация Compton
README в корне — только «как запустить за 30 секунд». Всё остальное — здесь.
Мы сознательно держим **мало файлов**, но каждый — **плотный**: таблицы, схемы, команды. Можно с лёгким юмором, но без простыней на 500 строк.
## Карта
| Документ | Когда открывать |
|----------|-----------------|
| [project.md](./project.md) | «Где что лежит?», архитектура, API, маршруты, env |
| [deploy.md](./deploy.md) | Запуск, **стандартные логины dev**, staging/prod, troubleshooting |
| [security.md](./security.md) | Auth, JWT revoke, секреты, nginx, чеклист prod |
| [release.md](./release.md) | Перед выкладкой: E2E, k6, ZAP, Lighthouse, smoke |
| [TZ.md](./TZ.md) | Полное ТЗ — источник правды по требованиям |
## Быстрые ссылки
```bash
# dev
python apps/api/scripts/bootstrap_install.py
docker compose --profile docker-web up -d --build
# тесты
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
# prod smoke (после деплоя)
./infra/scripts/smoke-prod.sh https://your-domain.com
```
## Что куда не кладём
| Не в git | Почему |
|----------|--------|
| `apps/api/data/secrets/install.env` | пароли БД, JWT, MinIO |
| `.env`, `apps/api/.env` | локальные секреты |
| `apps/api/data/logs/` | runtime-логи |
| `node_modules/`, `.venv/` | очевидно |
Если секрет утёк в git — считайте его скомпрометированным. Force-push не спасает совесть.
+196
View File
@@ -0,0 +1,196 @@
# Деплой и эксплуатация
От «запустил на ноуте» до «живёт на VPS и не стыдно показать security.md».
## Среды
```mermaid
flowchart LR
Dev[docker-compose.yml] --> St[staging]
St --> QA[k6 + ZAP + E2E]
QA --> Prod[production]
```
| Среда | Compose | APP_ENV | Docs | Demo users |
|-------|---------|---------|------|------------|
| Dev | `docker-compose.yml` | development | ✅ | ✅ |
| CI/E2E | `docker-compose.test.yml` | test | ✅ | ✅ |
| Staging | `infra/docker/docker-compose.staging.yml` | staging | ❌ | ❌ |
| Production | `infra/docker/docker-compose.prod.yml` | production | ❌ | ❌ |
## Локальная разработка
```bash
# 1. venv + зависимости backend (один раз)
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r apps/api/requirements-dev.txt
# 2. Секреты установки (один раз, до первого docker compose up)
python apps/api/scripts/bootstrap_install.py
# 3. Полный стек в Docker (API + БД + web)
docker compose --profile docker-web up -d --build
# 4. Проверка
curl http://localhost:8000/api/v1/health
```
| Сервис | URL |
|--------|-----|
| Web | http://localhost:5173 |
| API | http://localhost:8000 |
| PG/Redis/MinIO на хост | `docker-compose.dev-ports.yml` → 5432, 6379, 9000/9001 |
> Bootstrap и локальные тесты (`pytest`, `mypy`) — через активированный `.venv`. Docker API использует свой образ; venv нужен для скриптов и разработки на хосте.
**Гибрид** (инфра в Docker, frontend локально):
```bash
source .venv/bin/activate
docker compose up -d
pnpm install
pnpm --filter web dev
```
**Install secrets на хост** (DBeaver): Admin → Security → Install Secrets → Reveal DB password.
### Стандартные логины (dev)
Создаются при seed на старте API. Пароли по умолчанию — из `apps/api/.env.example` (или дефолты в `config.py`).
| Email | Пароль | Env | Роль | Superuser | Куда заходит |
|-------|--------|-----|------|:---------:|--------------|
| `admin@compton.example` | `Admin1234` | `ADMIN_INITIAL_PASSWORD` | admin | да | `/admin` — Users, Content, Security, Diagnostics, secrets |
| `ops@compton.example` | `OpsAdmin1234` | `DEMO_OPS_PASSWORD` | admin | нет | `/admin` — Users, Content, Activity (без Security) |
| `user@compton.example` | `User1234` | `DEMO_USER_PASSWORD` | user | — | `/profile` |
**CMS-страницы (seed):** `about`, `privacy`, `terms``/pages/about` и т.д.
> **Staging/production:** `SEED_DEMO_USERS=false` — demo `user@` и `ops@` **не создаются**, только admin + CMS. Пароль admin задаётся через `ADMIN_INITIAL_PASSWORD` **до первого seed**, потом — сменить в UI.
## Staging
**Нужно:** VPS, DNS, TLS certs в `infra/docker/certs/`, SMTP.
```bash
git clone https://git.groupkomton.ru/Matvey/site.git && cd site
python3 apps/api/scripts/bootstrap_install.py
cp infra/docker/.env.staging.example infra/docker/.env.staging
# правим: домен, CORS, SMTP, ADMIN_INITIAL_PASSWORD
./infra/docker/deploy-staging.sh
```
Проверка:
```bash
curl -fsS https://STAGING/api/v1/health
curl -fsS -o /dev/null -w "%{http_code}" https://STAGING/api/v1/docs # 404
./infra/scripts/health-check.sh https://STAGING
```
Nginx: `default.tls.conf` — 80→443, HSTS.
## Production
```bash
python3 apps/api/scripts/bootstrap_install.py
cp infra/docker/.env.production.example infra/docker/.env.production
./infra/docker/deploy-prod.sh infra/docker/.env.production
```
**Сразу после bootstrap:**
1. Бэкап `install.env` off-server (зашифровать)
2. Сменить пароль admin
| Сервис | Host ports |
|--------|------------|
| nginx | 80, 443 |
| api, web, pg, redis, minio | internal only |
```bash
./infra/scripts/smoke-prod.sh https://YOUR_DOMAIN
```
### Rollback
```bash
docker compose -f infra/docker/docker-compose.prod.yml down
git checkout PREVIOUS_TAG
./infra/docker/deploy-prod.sh infra/docker/.env.production
```
## Бэкапы и мониторинг
| Что | Команда / как |
|-----|---------------|
| PostgreSQL | `./infra/scripts/backup-postgres.sh``backups/postgres-*.sql.gz` |
| install.env | `cp …/install.env backups/install.env.$(date +%F).enc` + gpg |
| Uptime | `./infra/scripts/health-check.sh URL` или UptimeRobot на `/api/v1/health` + `/` |
| Логи | logrotate для `server.log`, `admin-audit.jsonl` |
## Восстановление секретов
`install.env` — единственный источник runtime-секретов. Потеряли — не генерируйте новый вслепую.
### Симптомы
- `password authentication failed for user "compton_app"`
- bootstrap после того, как Postgres volume уже создан
### Fix (есть бэкап)
```bash
docker compose down
# восстановить apps/api/data/secrets/install.env
docker compose up -d --build
```
### Fix (нет бэкапа)
| Вариант | Данные |
|---------|--------|
| Reveal из другой среды | сохраняются |
| `down -v` + bootstrap (**только dev**) | **удаляются все** |
```bash
docker compose --profile docker-web down -v
python apps/api/scripts/bootstrap_install.py
docker compose --profile docker-web up -d --build
```
## SMTP
Staging/prod: `EMAIL_DELIVERY_MODE=smtp`. Dev: `memory` (письма в RAM, SMTP не нужен).
| Env | Назначение |
|-----|------------|
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM` | сервер |
| `FRONTEND_URL` | ссылки в письмах |
## Troubleshooting
| Проблема | Решение |
|----------|---------|
| `install.env missing` | `python apps/api/scripts/bootstrap_install.py` |
| `password authentication failed` | [восстановление секретов](#восстановление-секретов) |
| Login failed | `curl …/health`, проверить `.env`, restart web |
| 401 refresh в консоли (гость) | норма на публичных страницах |
| Logout после F5 на `/admin` | rebuild web, перелогиниться |
| «На сайт» ведёт на login | должно быть `href="/"`, rebuild |
| Port 5173 busy | stop Docker web **или** local Vite |
| CORS | `VITE_USE_API_PROXY=true`, не бить напрямую :8000 |
| Нет Security/Diagnostics | логин `admin@`, не `ops@` |
| Docker web: missing modules | `docker compose … up -d --build web` |
## Compose-справочник
| Файл | Назначение |
|------|------------|
| `docker-compose.yml` | dev |
| `docker-compose.dev-ports.yml` | порты на хост |
| `docker-compose.test.yml` | CI/E2E |
| `infra/docker/docker-compose.staging.yml` | staging |
| `infra/docker/docker-compose.prod.yml` | production |
Deploy: `infra/docker/deploy-staging.sh`, `deploy-prod.sh`.
+220
View File
@@ -0,0 +1,220 @@
# Структура и архитектура
Monorepo-lite: статический лендинг + React SPA + FastAPI + PostgreSQL + Redis + MinIO.
Если вы искали микросервисы на Kubernetes — это другой коридор.
## Стек
| Слой | Технологии |
|------|------------|
| Frontend | React 19, TS, Vite, React Router, TanStack Query, Zustand, RHF+Zod, Ant Design |
| Backend | FastAPI, SQLAlchemy 2, Alembic, Pydantic v2 |
| Данные | PostgreSQL 16, Redis 7, MinIO |
| Инфра | Docker Compose, Nginx, GitHub Actions |
| Качество | Vitest, Playwright, pytest (≥90% / ≥85% cov) |
## Дерево репозитория
```
site/
├── apps/
│ ├── api/ # Backend
│ │ ├── app/
│ │ │ ├── core/ # crypto, jwt_denylist, redis, install_secrets…
│ │ │ ├── db/ # models, seed, migrations helpers
│ │ │ └── modules/ # auth, users, content, admin, media, test
│ │ ├── migrations/ # Alembic
│ │ ├── scripts/ # bootstrap_install.py, docker_entrypoint.py
│ │ ├── tests/
│ │ └── data/
│ │ ├── secrets/ # install.env (gitignore!)
│ │ └── logs/ # server.log, admin-audit.jsonl (gitignore)
│ └── web/
│ ├── index.html # лендинг /
│ ├── app.html # SPA entry
│ ├── main/ # статика лендинга (CSS/JS/video)
│ ├── src/
│ │ ├── app/ # router, guards
│ │ ├── modules/ # auth, profile, admin, content, landing
│ │ ├── pages/
│ │ └── shared/ # api client, ui
│ └── e2e/ # Playwright
├── packages/ # eslint-config, shared-types (target)
├── infra/
│ ├── docker/ # staging/prod compose, deploy.sh
│ ├── nginx/ # default.conf, default.tls.conf
│ ├── k6/ # load test §17.2
│ └── scripts/ # backup, health, smoke
├── docs/ # вы здесь
├── docker-compose.yml # dev
└── docker-compose.test.yml # CI / E2E
```
## Runtime
```mermaid
flowchart TB
subgraph browser [Браузер]
L[index.html /]
S[app.html SPA]
end
subgraph edge [Nginx :80/:443]
N[TLS + headers]
end
subgraph internal [Docker internal]
W[web]
A[api]
PG[(PostgreSQL)]
R[(Redis)]
M[(MinIO)]
end
L --> N
S --> N
N --> W
N --> A
A --> PG
A --> R
A --> M
```
**Prod/staging:** наружу только nginx. Postgres, Redis, MinIO — без host-портов.
## Frontend
### Два входа (dual-entry)
| Entry | URL | Содержимое |
|-------|-----|------------|
| `index.html` | `/` | Маркетинговый лендинг (`main/`) |
| `app.html` | `/login`, `/admin`, … | React SPA |
Vite переписывает SPA-пути на `app.html` (`vite.main-static.ts`).
### Маршруты
| Путь | Guard | Кто |
|------|-------|-----|
| `/` | — | все |
| `/login`, `/register`, `/forgot-password`, `/reset-password` | GuestGuard | гости |
| `/verify`, `/pages/:slug` | — | публично |
| `/profile` | AuthGuard | user |
| `/admin` | AdminGuard | admin |
**Auth UX:**
- Access JWT — только в памяти (Zustand), не localStorage
- Refresh — HttpOnly cookie, `Path=/api/v1/auth`
- После login: admin → `/admin`, user → `/profile`
- Кнопка «На сайт» в админке — **полный** переход на `/` (не React Router)
### Админка (WESP-style)
| Раздел | Кому | Что |
|--------|------|-----|
| Users | admin | CRUD пользователей |
| Content | admin | CMS |
| Security | superuser | runtime settings, Install Secrets |
| Diagnostics | superuser | health checks |
| Activity | admin | audit feed, server log |
Тема Light/Dark — `localStorage.wespAdminTheme`. Auth-страницы — zootech-карточки (`#48816d`).
## Backend API
База: `/api/v1`
| Модуль | Эндпоинты (основное) |
|--------|----------------------|
| health | `GET /health` |
| auth | register, login, logout, refresh, verify, forgot/reset password |
| users | `GET/PATCH /me`, password, avatar |
| content | публичные pages + admin CRUD |
| admin | users, stats, settings, diagnostics, secrets, activity |
| media | подписанные URL файлов |
| test | `/test/emails/latest-token`**только** E2E |
### Core (`apps/api/app/core/`)
| Модуль | Зачем |
|--------|-------|
| `crypto.py` | bcrypt, JWT, HMAC, token hash — одна точка |
| `jwt_denylist.py` | мгновенный revoke access JWT |
| `install_secrets.py` | bootstrap + lock |
| `dependencies.py` | `get_current_user` |
| `redis.py` | rate limit + JWT revoke |
| `storage.py` | MinIO / memory |
## База данных
```mermaid
erDiagram
users ||--o| user_profiles : has
users ||--o{ refresh_tokens : owns
users ||--o{ password_reset_tokens : owns
users ||--o{ email_verification_tokens : owns
```
Таблицы: `users`, `user_profiles`, `refresh_tokens`, `password_reset_tokens`, `email_verification_tokens`, `content_pages`.
```bash
cd apps/api && alembic upgrade head
```
CHECK constraints на `role`, `status`; superuser только при `role=admin`. Cleanup expired tokens при старте API.
### Seed: стандартные логины (dev)
| Email | Пароль | Env |
|-------|--------|-----|
| admin@compton.example | Admin1234 | `ADMIN_INITIAL_PASSWORD` |
| ops@compton.example | OpsAdmin1234 | `DEMO_OPS_PASSWORD` |
| user@compton.example | User1234 | `DEMO_USER_PASSWORD` |
CMS: `about`, `privacy`, `terms`. Подробнее — [deploy.md § логины](./deploy.md#стандартные-логины-dev).
## Переменные окружения (ключевые)
| Переменная | Где | Назначение |
|------------|-----|------------|
| `DATABASE_URL` | api | PostgreSQL |
| `APP_ENV` | api | development / staging / production |
| `JWT_ACCESS_SECRET`, `JWT_REFRESH_PEPPER` | api | токены |
| `REDIS_URL` | api | rate limit + JWT revoke (prod обязателен) |
| `SEED_DEMO_USERS` | api | `false` на staging/prod |
| `ADMIN_INITIAL_PASSWORD` | api | пароль admin при seed (default `Admin1234`) |
| `DEMO_USER_PASSWORD`, `DEMO_OPS_PASSWORD` | api | demo user/ops (только dev) |
| `ENABLE_DOCS` | api | `false` на prod |
| `ENABLE_TEST_ROUTES` | api | `true` только E2E |
| `EMAIL_DELIVERY_MODE` | api | `memory` (dev) / `smtp` (prod) |
| `STORAGE_MODE` | api | `s3` / `memory` |
| `VITE_API_URL` | web | `http://api:8000` в Docker |
| `VITE_USE_API_PROXY` | web | `true` в dev |
Полные примеры: `apps/api/.env.example`, `apps/api/.env.production.example`.
### Runtime settings
`apps/api/data/compton_settings.json` — toggles без секретов. Superuser: `GET/PATCH /admin/settings`. Env с тем же ключом = **lock** (нельзя менять из UI).
## Docker Compose
| Файл | Когда |
|------|-------|
| `docker-compose.yml` | локальная разработка |
| `docker-compose.dev-ports.yml` | PG/Redis/MinIO на хост (DBeaver) |
| `docker-compose.test.yml` | CI, Playwright |
| `infra/docker/docker-compose.staging.yml` | staging VPS |
| `infra/docker/docker-compose.prod.yml` | production VPS |
## CI
`.github/workflows/ci.yml`: lint → types → mypy → tests → audit → Bandit → gitleaks → E2E.
## Тесты
```bash
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
pnpm --filter web e2e
```
E2E поднимает API `:8001` + Vite `:5175`. Против staging: `E2E_BASE_URL=… E2E_START_API=false`.
-14
View File
@@ -1,14 +0,0 @@
# MVP Regression Checklist
This checklist mirrors the required release scenarios.
1. Landing hero/marquee and reduced-motion behavior.
2. Register -> verify -> login -> profile edit -> logout.
3. Forgot password -> reset -> login.
4. Admin publish content -> public slug availability.
5. Admin blocks user -> blocked user login denied.
6. Pending user cannot access `/profile`.
7. Refresh token rotation and old token rejection.
8. IDOR check: user A cannot access user B.
9. Admin cannot demote/block self; last admin protected.
10. Avatar upload rejects invalid MIME/oversize/SVG.
+113
View File
@@ -0,0 +1,113 @@
# Релиз и QA gates
Перед production не «авось прокатит», а чеклист из ТЗ §17. Если что-то красное — сначала staging, потом prod. Живёт один раз.
## Pipeline
```mermaid
flowchart TD
CI[CI green] --> ST[Staging + TLS]
ST --> E2E[E2E 11/11]
ST --> K6[k6 pass]
ST --> ZAP[ZAP 0 High/Crit]
ST --> LH[Lighthouse ≥ 85]
E2E --> PROD[Production deploy]
K6 --> PROD
ZAP --> PROD
LH --> PROD
PROD --> SM[smoke-prod.sh]
SM --> DNS[DNS cutover]
DNS --> MON[24h мониторинг]
```
## Регрессия E2E
**Последний локальный прогон:** 2026-07-14 — backend 137 / 90.43% cov, frontend 46.
### Staging
```bash
E2E_BASE_URL=https://staging.example.com E2E_START_API=false pnpm --filter web e2e
```
| # | Сценарий | Local | Staging |
|---|----------|:-----:|:-------:|
| 1 | Landing hero, reduced-motion | ☐ | ☐ |
| 2 | Register → verify → login → profile → logout | ☐ | ☐ |
| 3 | Forgot → reset → login | ☐ | ☐ |
| 4 | Admin publish → public slug | ☐ | ☐ |
| 5 | Block user → login denied | ☐ | ☐ |
| 6 | Pending → нет `/profile` | ☐ | ☐ |
| 7 | Refresh rotation | ☐ | ☐ |
| 8 | IDOR user A ≠ user B | ☐ | ☐ |
| 9 | Admin не блокирует себя / last admin | ☐ | ☐ |
| 10 | Avatar: bad MIME / size / SVG | ☐ | ☐ |
| 11 | Block → access JWT 401 TOKEN_REVOKED | ☐ | ☐ |
### Локально
```bash
pnpm --filter web e2e # API :8001, Vite :5175
```
## k6 (§17.2)
```bash
k6 run infra/k6/mvp-load-test.js -e BASE_URL=https://staging.example.com
```
| Параметр | Порог |
|----------|-------|
| VU / ramp | 50 / 5 min |
| Mix | 35% list, 25% login, 20% me, 10% refresh, 10% slug |
| p95 | < 300 ms |
| Errors | < 1% |
## OWASP ZAP
```bash
docker run --rm -v "$(pwd):/zap/wrk:rw" -t ghcr.io/zap/zaproxy:stable \
zap-baseline.py -t https://staging.example.com -r zap-report.html
```
| Severity | Pass |
|----------|------|
| High, Critical | **0** |
Medium/Low — review руками. `zap-report.html` — в архив релиза.
## Lighthouse
```bash
npx lighthouse https://staging.example.com \
--preset=mobile --only-categories=performance \
--output=json --output-path=./lighthouse-report.json
```
| Метрика | ≥ |
|---------|---|
| Performance (mobile, `/`) | 85 |
Не прошло — hero video `preload="none"`, font swap, меньше JS на лендинге.
## Release gates (сводка)
- [ ] CI green на `main`
- [ ] E2E 11/11 на staging
- [ ] k6 pass
- [ ] ZAP 0 High/Critical
- [ ] Lighthouse ≥ 85
- [ ] [security.md](./security.md) staging-пункты
- [ ] `./infra/scripts/smoke-prod.sh` на prod
- [ ] DNS cutover + rollback plan (previous tag)
## CI локально
```bash
pnpm lint && pnpm typecheck
cd apps/api && python -m mypy app
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
```
Полный pipeline: `.github/workflows/ci.yml`.
-30
View File
@@ -1,30 +0,0 @@
# Install Secrets Recovery
This project keeps runtime installation secrets in `apps/api/data/secrets/install.env`.
## Important
- Do not rotate `POSTGRES_PASSWORD`, `JWT_ACCESS_SECRET`, or `JWT_REFRESH_PEPPER` automatically after first bootstrap.
- A mismatch between `install.env` and initialized Postgres volume can break database access.
## Safe recovery steps
1. Stop services:
- `docker compose down`
2. Restore `apps/api/data/secrets/install.env` from backup.
3. Start services:
- `docker compose up -d --build`
If backup is unavailable, you have two options:
- Preferred: recover credentials directly from running database/admin secret reveal in another environment.
- Last resort: reset local volumes and lose local dev data:
- `docker compose down -v`
- `python apps/api/scripts/bootstrap_install.py`
- `docker compose up -d --build`
## Dev access ports
To expose DB/Redis/MinIO to host tools:
- `docker compose -f docker-compose.yml -f docker-compose.dev-ports.yml up -d`
-20
View File
@@ -1,20 +0,0 @@
# Security Checklist (MVP Pre-Production)
- [x] Access JWT is memory-only in frontend state (no sessionStorage/localStorage persistence).
- [x] Refresh token is HttpOnly/Secure/SameSite cookie on `/api/v1/auth` path.
- [x] Auth endpoints implemented with neutral anti-enumeration messaging.
- [x] Origin/Referer validation is enforced for auth endpoints.
- [x] Authenticated/admin route guards and role checks are enforced, including `SUPERUSER_ONLY` checks for critical endpoints.
- [x] Content sanitization is enabled for CMS HTML body.
- [x] Security headers configured in `infra/nginx/default.conf`.
- [x] CI includes dependency audit, Bandit, and gitleaks scans.
- [x] Settings runtime supports `data/compton_settings.json` with env lock behavior.
- [x] Admin audit feed is persisted to `data/logs/admin-audit.jsonl`.
- [x] Install secrets bootstrap is enabled (`apps/api/data/secrets/install.env`) and locked after first run.
- [x] Database, Redis and MinIO are internal by default in base docker compose.
- [x] `refresh` validates user status and rate limit is checked before token rotation.
- [x] CMS sanitization enforces allowed URL protocols (`http`, `https`, `mailto`).
- [x] Admin password create/reset uses shared password policy validators.
- [ ] Production docs endpoint switch (`ENABLE_DOCS=false`) validated in staging/prod env.
- [ ] HSTS behavior validated behind TLS ingress in staging/prod.
- [ ] Recovery runbook for lost `install.env` tested (`docs/secrets-recovery.md`).
+173
View File
@@ -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 на `/`