food-market/docs/food-market-server-postgres-role.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

121 lines
6.4 KiB
Markdown
Raw 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.

# Замена 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 после старта.
```
## Проверка работоспособности
```bash
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 в логах), вернуться
к старой конфигурации одной командой:
```bash
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`), её можно дропнуть отдельно:
```sql
-- Только после успешного rollback'а и подтверждения что приложение
-- работает на postgres:
DROP OWNED BY food_market_server_app; -- (если бы что-то создавала)
DROP ROLE food_market_server_app;
```
## Что НЕ покрыто (TODO)
- **Ротация пароля postgres**. Сам `postgres` superuser остался с тем
же `1q2w3e4r`. Пока он не используется в работе app'а (мы только
что переключились на app-роль) — но всё равно нужно сменить на
сильный, иначе кто-то с доступом к dev-машине или к /opt видит
старый бэкап и пробует. Делать через `ALTER ROLE postgres PASSWORD
'...'` под superuser-сессией.
- **PGHBA**. Сейчас доверяется любой коннект с loopback (стандартный
`pg_hba.conf` postgres-контейнера). Допустимо для 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_USER` env, что даёт superuser в рамках
только этой БД — лучше чем кросс-БД superuser, но всё равно стоит
ужесточить аналогично).
- `food-market-postgres` (prod admin.food-market.kz, port 5434) — то же.
- Локальный dev PG на host'е (brew postgresql@14) — там безразлично
(dev-only, локальный сокет, пустой пароль работает по trust).