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>
8.5 KiB
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.
Чек-лист
- 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. - 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). - 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. - 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) сохранён. - 5. Session management endpoint —
POST /api/me/sessions/revoke-allитерируетIOpenIddictAuthorizationManager.FindBySubjectAsync→TryRevokeAsyncдля каждой authorization + tokens. Возвращает{revokedAuthorizations, revokedTokens}. Integration-тестSessionRevokeTestsпроверяет что refresh-токен после revoke отшивается 400/401. - 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. - 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-копию».