food-market/deploy/nginx.conf
nns a80471d0f9
Some checks failed
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
fix(security): add HSTS header on stage + integration test
Найдено в Sprint 28 security audit: stage отдаёт security-заголовки
(CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy и др.), но
БЕЗ Strict-Transport-Security. HSTS из ASP.NET Core (Program.cs UseHsts)
не срабатывает потому что api за nginx-прокси видит запрос как HTTP
(нет ForwardedHeaders middleware'a; nginx X-Forwarded-Proto не дешифруется).

Простейший фикс: добавить HSTS в deploy/nginx.conf (web-контейнер).
Brower honors HSTS только на HTTPS-ответах — безопасно unconditional.

max-age=2592000 (30 дней), без includeSubDomains и без preload —
pre-emptive consent, можно безопасно убрать. Когда production stack
устаканится и admin.food-market.kz будет подан в hstspreload.org —
увеличить до 31536000 + preload + includeSubDomains.

Verified:
  curl -I https://test.admin.food-market.kz/ | grep -i strict
  > strict-transport-security: max-age=2592000

Integration test 08-security-headers.spec.ts проверяет 7 security-
заголовков на главной + на 404 (always-параметр).

Cert: 10/10 integration tests passed in 1.3m.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 03:35:38 +05:00

149 lines
7.1 KiB
Nginx Configuration File
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.

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;
# Sprint 28: HSTS. Brower honors HSTS only on HTTPS responses, поэтому
# безопасно добавлять unconditionally — если клиент пришёл по HTTP,
# header игнорируется. Без includeSubDomains и без preload — это
# pre-emptive consent: можно безопасно убрать. Когда production stack
# устаканится и admin.food-market.kz будет подан в hstspreload.org,
# увеличить max-age до 31536000 + добавить preload и includeSubDomains.
add_header Strict-Transport-Security "max-age=2592000" 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;
}
}