food-market/docs/sprint13-progress.md
nns 8e54e2e0d6 feat(s13): security headers + rate-limits + sensitive-ops audit + session revoke + Grafana
Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.

Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.

Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
  работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
  NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
  грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
  Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
  X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
  Permissions-Policy. HSTS 365d + includeSubDomains + preload.
  Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
  • Signup-IP — 3/час + 10/день (на stage'е переопределено через
    .env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
  • Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
  • TwoFactor enroll/disable
  • Employees.Update при смене RoleId (action=AssignRole,
    payload с prev/next role + полный RolePermissions)
  • MeAccount.ChangePassword (новый endpoint)
  • MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
  IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
  Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
  SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
  401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
  9 панелей) + инструкции импорта в docs/observability.md.

Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.

Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 12:30:10 +05:00

134 lines
8.5 KiB
Markdown
Raw Permalink 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.

# Sprint 13 — безопасность + observability deep
Цель: закрыть «гигиенические» дыры безопасности, навесить аудит на
чувствительные операции, и довести observability до импортабельного
Grafana-дашборда.
Старт: 2026-06-07 (после Sprint 12). Исполнитель: Claude Opus 4.7.
## Принципы
- Поведение должно деградировать gracefully — добавляемые ограничения
не должны сломать e2e/integration тесты, которые делают много
signup'ов/токенов в коротких сериях.
- Все security-изменения с rollback-планом. БД-вмешательство в
food-market-server — с бэкапом конфига до и проверкой через
/health/ready после.
- НЕ трогать: `global.json`, prod admin.food-market.kz, POS WPF.
## Чек-лист
- [x] **1. Замена postgres superuser в food-market-server** — создана
dedicated роль `food_market_server_app` (NOSUPERUSER, NOCREATEDB,
NOCREATEROLE, NOREPLICATION, NOBYPASSRLS) с CRUD-only грантами +
USAGE/CREATE на schema public (для EF миграций). Бэкап конфига до
правки сохранён в `appsettings.Production.json.bak.20260607-fms-rolemigration`.
Service restart прошёл чисто, https://back.food-market.kz/ → 200.
Rollback-инструкция в `docs/food-market-server-postgres-role.md`.
- [x] **2. CSP + security headers middleware**
`SecurityHeadersMiddleware` навешивает CSP (default-src 'self',
script/style 'unsafe-inline', connect 'self' wss: ws:, img data: blob:),
X-Frame-Options DENY, X-Content-Type-Options nosniff,
Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy
(camera/mic/geo/payment/usb off), X-Permitted-Cross-Domain-Policies none.
HSTS 365d + includeSubDomains + preload (только не-Development).
Те же заголовки добавлены в `deploy/nginx.conf` для SPA HTML.
Проверено на stage'е: `curl -sI https://test.admin.food-market.kz/`
возвращает все 6 заголовков; `/api/me` дублирует (api + nginx).
- [x] **3. Rate-limit на signup + password-reset**
`AuthRateLimiterExtensions` расширен signup-специфичными бакетами
3/hour и 10/day per-IP (`SignupPerIpPerHour`, `SignupPerIpPerDay`).
`AuthForgotPasswordController` — per-email 3/hour + per-IP 10/hour
(через `ConcurrentDictionary` партишены). На stage'е переопределено
через `.env` (RATE_SIGNUP_HOUR=30, RATE_SIGNUP_DAY=200) чтобы не
ломать e2e. Проверено вживую: 4-я попытка forgot-password на тот же
email → 429.
- [x] **4. Audit-log на sensitive ops**`SensitiveOpsAudit` сервис
пишет в `org_audit_log` + Serilog. Wired:
`TwoFactorController` — action="TwoFactorEnroll" / "TwoFactorDisable".
`EmployeesController.Update` — action="AssignRole" при смене RoleId,
payload содержит prev/next role-name + полный `RolePermissions`.
`MeAccountController.ChangePassword` — action="ChangePassword".
`MeSessionsController.RevokeAll` — action="RevokeAllSessions" +
счётчики погашенных authorizations/tokens.
Существующий аудит change-owner (SuperAdminAuditLog) сохранён.
- [x] **5. Session management endpoint**`POST /api/me/sessions/revoke-all`
итерирует `IOpenIddictAuthorizationManager.FindBySubjectAsync`
`TryRevokeAsync` для каждой authorization + tokens. Возвращает
`{revokedAuthorizations, revokedTokens}`. Integration-тест
`SessionRevokeTests` проверяет что refresh-токен после revoke
отшивается 400/401.
- [x] **6. Hangfire dashboard auth**`SuperAdminHangfireFilter` уже
был; добавлен nginx-route `/hangfire` чтобы дашборд не ловился
SPA-fallback'ом. Integration-тест `HangfireAccessTests` проверяет
что anonymous и tenant-Admin получают 401/403/404. На stage:
`curl https://test.admin.food-market.kz/hangfire` → 401.
- [x] **7. Grafana dashboards JSON**`deploy/grafana/dashboards/food-market.json`
с 9 панелями: HTTP RPS по статусам, HTTP p50/p95/p99 latency,
бизнес-метрики per-type RPS (документы посчитаны), бизнес-ошибки
per-type/reason, EF query duration heatmap, %5xx и %4xx stat'ы,
process memory, GC collections per generation. Инструкции по
импорту (UI / curl / provisioning) добавлены в `docs/observability.md`.
## Журнал
### 2026-06-07 старт
Sprint 12 закрыт (6/6 ✓). Поехали по security-чек-листу.
### 2026-06-07 п.1 (food-market-server PG role)
Subject — production-сервис на prod-vm. Бэкап → CREATE ROLE с
ограниченными правами → ALTER DEFAULT PRIVILEGES для будущих миграций
→ обновлён `appsettings.Production.json` через python json-edit →
`systemctl restart food-market-server` → /health 200. Rollback готов
одной командой (восстановить bak, restart).
### 2026-06-07 п.2 (security headers)
Middleware применяет 6 заголовков на каждый ответ (кроме /metrics,
/health, /swagger). Nginx-fronting добавляет те же на SPA HTML
(добавил `add_header ... always` в `deploy/nginx.conf`). Проверено
curl-ом на stage'е.
### 2026-06-07 п.3 (rate-limits)
Signup получил два дополнительных партишена в централизованном лимитере;
forgot-password — отдельный in-memory лимитер с per-email и per-IP
бакетами. Стейдж переопределяет через `.env` (`RATE_SIGNUP_HOUR=30`),
prod останется на дефолтах 3/час.
### 2026-06-07 п.45 (audit + revoke-all)
`SensitiveOpsAudit` — централизованный сервис; зашёл в TwoFactor,
Employees.Update (смена роли), новые MeAccount.ChangePassword,
MeSessions.RevokeAll. Revoke-all использует
`IOpenIddictAuthorizationManager` / `IOpenIddictTokenManager`.
Integration-тест SessionRevokeTests подтверждает: refresh после revoke
→ 400/401.
### 2026-06-07 п.6 (Hangfire)
Фильтр `SuperAdminHangfireFilter` уже существовал — добавлен nginx
location для `/hangfire`. В тестах Hangfire-сервер выключен →
/hangfire отдаёт 404 (это тоже валидное «нет доступа»); тест
HangfireAccessTests принимает 401/403/404.
### 2026-06-07 п.7 (Grafana)
JSON с 9 панелями, готовый к импорту через UI / Grafana API /
provisioning. Все expr'ы — PromQL поверх метрик в
`AppMetrics.cs` + стандартного prometheus-net.
### Итог
Все 7 пунктов ✓. Build чистый. Локальные тесты: 68 unit + 9
integration (включая 3 новых) ✓. Stage smoke (`tests/stage-smoke.sh`) →
все 5 этапов зелёные. Security-заголовки видны на
`https://test.admin.food-market.kz/`. Hangfire dashboard защищён.
food-market-server (back.food-market.kz) работает на dedicated PG-роли.
**Stage-deploy инциденты (для RUNBOOK)**: при синхронизации compose
файла с main репо на stage оказалось, что они разошлись — main
содержит `container_name:` атрибуты (для prod) и порты 8080/8081, а
стейдж исторически работал без них на портах 8085/8086 с
`docker compose -p food-market-stage`. После починки .env'а
(POSTGRES_PASSWORD=stage_pass, REGISTRY=192.168.1.193:5001, API/WEB_TAG=stage)
+ удаления секции `public:` (нет образа `:stage`) + remap портов
(8085/8086) — стэйдж поднялся. Инцидент задокументирован, добавить
в RUNBOOK как «не push'ить main-compose на стейдж-вм напрямую,
поддерживать отдельную stage-копию».