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>
134 lines
8.5 KiB
Markdown
134 lines
8.5 KiB
Markdown
# 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 п.4–5 (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-копию».
|