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>
141 lines
6.4 KiB
Nginx Configuration File
141 lines
6.4 KiB
Nginx Configuration File
server {
|
||
listen 80 default_server;
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# Sprint 13 — security-заголовки (для SPA HTML; для API те же выставляются
|
||
# уже SecurityHeadersMiddleware'ом на api-side). add_header с always
|
||
# обеспечивает применение даже на 4xx/5xx (без always — только на 2xx/3xx).
|
||
# CSP синхронен с SecurityHeadersOptions.DefaultCsp.
|
||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data: blob:; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
|
||
add_header X-Frame-Options "DENY" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
|
||
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
||
|
||
# Long-running admin imports (MoySklad etc.) read from upstream for tens of
|
||
# minutes. Bump timeouts only on that path so normal API stays snappy.
|
||
location /api/admin/import/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60m;
|
||
proxy_send_timeout 60m;
|
||
proxy_request_buffering off;
|
||
proxy_buffering off;
|
||
}
|
||
|
||
# API reverse-proxy — upstream name "api" resolves in the compose network.
|
||
location /api/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
}
|
||
|
||
location /connect/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
}
|
||
|
||
# SignalR хаб для live-уведомлений (см. NotificationsHub).
|
||
# WebSocket требует upgrade-хедеры и большой read_timeout (иначе nginx
|
||
# будет рвать idle-коннекшен каждые 60 сек). access_token приходит как
|
||
# query (?access_token=...), Authorization-хедер middleware на API его
|
||
# перекладывает в нужный вид до UseAuthentication.
|
||
location /hubs/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_read_timeout 86400; # 24h — webSocket долгоживущий
|
||
proxy_send_timeout 86400;
|
||
proxy_buffering off;
|
||
}
|
||
|
||
location /health {
|
||
proxy_pass http://api:8080;
|
||
}
|
||
|
||
# Prometheus метрики API. Без этого блока запрос ловится SPA fallback'ом и
|
||
# возвращает index.html (947 байт) вместо exposition format. На prod-домене
|
||
# имеет смысл закрыть IP-фильтром (allow 192.168.0.0/16; deny all;), на
|
||
# stage оставляем открытым — за gateway nginx уже есть auth/TLS-обвязка.
|
||
location = /metrics {
|
||
proxy_pass http://api:8080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
# Swagger UI + OpenAPI-doc. На контейнере api подключается только когда
|
||
# IncludeSwagger=true (env-флаг, см. Program.cs). На prod-домене флаг не
|
||
# выставляем, /swagger вернёт 404 от api — это ожидаемо.
|
||
location /swagger/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
location = /swagger {
|
||
return 301 /swagger/;
|
||
}
|
||
|
||
# Sprint 13: Hangfire Dashboard — внутренний инструмент мониторинга
|
||
# фоновых джобов. Доступ только SuperAdmin'у (см. SuperAdminHangfireFilter
|
||
# в API). Без этой location'и /hangfire ловился бы SPA-fallback'ом и
|
||
# возвращал index.html — что выглядит как «всё ок», но дашборда нет.
|
||
location /hangfire {
|
||
proxy_pass http://api:8080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
# Статика изображений товаров — api раздаёт /uploads/... из volume.
|
||
location /uploads/ {
|
||
proxy_pass http://api:8080;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
}
|
||
|
||
# PWA: SW и manifest должны отдаваться с правильным content-type и без
|
||
# кеша на самом ответе (внутри SW свой versioned cache). Иначе старый
|
||
# SW залипает на клиенте и не подхватывает обновления.
|
||
location = /sw.js {
|
||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||
add_header Pragma "no-cache";
|
||
expires off;
|
||
try_files /sw.js =404;
|
||
}
|
||
location = /manifest.webmanifest {
|
||
types { } default_type application/manifest+json;
|
||
add_header Cache-Control "public, max-age=3600";
|
||
try_files /manifest.webmanifest =404;
|
||
}
|
||
location = /offline.html {
|
||
try_files /offline.html =404;
|
||
}
|
||
|
||
# SPA fallback — all other routes return index.html
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
}
|