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>
6.4 KiB
6.4 KiB
Замена postgres superuser в food-market-server
Sprint 13, задача 1. Дата: 2026-06-07.
Контекст
food-market-server — legacy backend (back.food-market.kz, port 8084
на prod-vm 192.168.1.190, systemd food-market-server.service).
Хранилище — food-market-server-postgres (Docker, port 5436).
До этой задачи в appsettings.Production.json была строка с
superuser'ом:
Host=localhost;Port=5436;Database=food_market_server;Username=postgres;Password=1q2w3e4r
Это плохо по двум причинам:
- Слабый пароль (
1q2w3e4r), известен любому, кто прочитает конфиг. postgres— суперюзер: CREATE DATABASE, CREATE ROLE, REPLICATION, BYPASS RLS, может уничтожить всё что угодно в кластере (включая другие БД, если они там появятся).
Решение
Создан dedicated app-role food_market_server_app:
- LOGIN + сильный пароль (48 hex chars).
- NOSUPERUSER, NOCREATEDB, NOCREATEROLE, NOREPLICATION, NOBYPASSRLS.
- Гранты: SELECT/INSERT/UPDATE/DELETE на все существующие таблицы +
USAGE/SELECT/UPDATE на sequences + USAGE/CREATE на schema public
(CREATE нужен для EF миграций, которые app запускает на старте через
db.Database.Migrate()). - DEFAULT PRIVILEGES
FOR ROLE postgres IN SCHEMA public— все будущие таблицы, что создаст superuser (например, если миграцию применить вручную черезpostgres), автоматически получат CRUD для app-роли.
Что сделано (атомарно)
1. Бэкап конфига:
/opt/food-market-server/appsettings.Production.json
→ appsettings.Production.json.bak.20260607-fms-rolemigration
2. Создание роли в БД:
CREATE ROLE food_market_server_app LOGIN PASSWORD '...'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
GRANT CONNECT ON DATABASE food_market_server TO food_market_server_app;
GRANT USAGE, CREATE ON SCHEMA public TO food_market_server_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO food_market_server_app;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO food_market_server_app;
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO food_market_server_app;
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO food_market_server_app;
3. Обновление appsettings.Production.json:
Username: postgres → food_market_server_app
Password: 1q2w3e4r → <48-hex>
4. systemctl restart food-market-server → active.
5. curl http://localhost:8084/ → 200 (SPA fallback).
6. curl https://back.food-market.kz/ → 200.
7. Логи без EF errors после старта.
Проверка работоспособности
ssh nns@192.168.1.190 'sudo systemctl status food-market-server | head -5'
curl -fsS https://back.food-market.kz/ -o /dev/null -w "HTTP %{http_code}\n"
ssh nns@192.168.1.190 'sudo journalctl -u food-market-server --since "10 minutes ago" --no-pager | grep -iE "error|fail" | head'
Rollback
Если что-то сломается (миграция fails, EF Errors в логах), вернуться к старой конфигурации одной командой:
ssh nns@192.168.1.190 'sudo cp /opt/food-market-server/appsettings.Production.json.bak.20260607-fms-rolemigration /opt/food-market-server/appsettings.Production.json && sudo systemctl restart food-market-server'
curl -fsS https://back.food-market.kz/ -o /dev/null -w "HTTP %{http_code}\n"
Это вернёт Username=postgres;Password=1q2w3e4r — старый superuser.
Новая роль food_market_server_app остаётся в БД (это идемпотентный
CREATE IF NOT EXISTS), её можно дропнуть отдельно:
-- Только после успешного rollback'а и подтверждения что приложение
-- работает на postgres:
DROP OWNED BY food_market_server_app; -- (если бы что-то создавала)
DROP ROLE food_market_server_app;
Что НЕ покрыто (TODO)
- Ротация пароля postgres. Сам
postgressuperuser остался с тем же1q2w3e4r. Пока он не используется в работе app'а (мы только что переключились на app-роль) — но всё равно нужно сменить на сильный, иначе кто-то с доступом к dev-машине или к /opt видит старый бэкап и пробует. Делать черезALTER ROLE postgres PASSWORD '...'под superuser-сессией. - PGHBA. Сейчас доверяется любой коннект с loopback (стандартный
pg_hba.confpostgres-контейнера). Допустимо для single-host setup, но при сетевом расширении нужно ужесточать. - Audit log внутри PG (pgaudit) — не настроен. Логирует только app.
- Per-table RLS — не используется, потому что приложение само
фильтрует по
OrganizationId. Под добавление RLS не подписывался.
Дальше
Аналогичную замену нужно провести в:
food-market-stage-postgres(port 5435) — там пользовательfood_marketуже без superuser-прав (см.deploy/docker-compose.yml, он создаётся черезPOSTGRES_USERenv, что даёт superuser в рамках только этой БД — лучше чем кросс-БД superuser, но всё равно стоит ужесточить аналогично).food-market-postgres(prod admin.food-market.kz, port 5434) — то же.- Локальный dev PG на host'е (brew postgresql@14) — там безразлично (dev-only, локальный сокет, пустой пароль работает по trust).