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>
121 lines
6.4 KiB
Markdown
121 lines
6.4 KiB
Markdown
# Замена 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).
|