Files
chatballs/deploy/nginx/frontend.production.conf
AndreyandClaude Opus 5 a321109a8f 🐛 fix(gateway): установка на нестандартном порту принимает формы
Шлюз передавал дальше Host без порта ({host} в Caddy, $host в nginx). Браузер
при этом шлёт Origin с портом, и CSRF-проверка Django отвергала любой POST на
установке, опубликованной как ip:8081: «Origin checking failed».

Теперь Host уходит с портом. X-Forwarded-Proto и X-Forwarded-For Caddy
принимает от прокси из частных сетей — установку часто ставят за прокси
панели, который снимает TLS и ходит к шлюзу по http; без этого CSRF падал бы
снова, как только перед установкой появлялся https.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:32:25 +03:00

99 lines
4.9 KiB
Plaintext
Raw Permalink 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;
server_name _;
# Имена сервисов разрешаются на каждом запросе через DNS Docker, а не один
# раз при старте nginx: после пересоздания контейнера (compose up, обновление
# образа) адрес меняется, и закэшированный IP уводил бы трафик на другой
# сервис. Для этого upstream задан переменной — с ней nginx резолвит заново
# по истечении valid.
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend_app backend-app:8000;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 20m;
# nginx НЕ наследует add_header в location, где есть свой add_header —
# он заменяет весь набор. Поэтому эти два заголовка продублированы в
# каждой такой location: без дубля SPA и страница виджета оставались без
# nosniff, и вложение с чужим Content-Type браузер додумывал сам.
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location = /chat-widget.js {
proxy_pass http://$backend_app;
proxy_http_version 1.1;
proxy_set_header Host $http_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 $http_x_forwarded_proto;
}
# Готовность стека — внутренний сигнал: её спрашивают healthcheck'и и
# smoke изнутри сети. Снаружи она сообщала бы состояние базы и Redis
# любому желающему. Liveness (/health/live/) остаётся открытым: по нему
# балансировщик отличает живой контейнер от мёртвого.
location = /api/v1/health/ready/ {
return 404;
}
location /api/ {
proxy_pass http://$backend_app;
proxy_http_version 1.1;
proxy_set_header Host $http_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 $http_x_forwarded_proto;
}
# WebSocket signaling звонков (SPEC-CHATBALLS-0013 §9).
location /ws/ {
proxy_pass http://$backend_app;
proxy_http_version 1.1;
proxy_set_header Host $http_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 $http_x_forwarded_proto;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
# Defense in depth: Django admin never crosses the app public boundary.
location = /admin {
return 404;
}
location /admin/ {
return 404;
}
location /chat/ {
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors *" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
try_files $uri $uri/ /chat/index.html;
}
# Клиентская страница звонка /calls/<invite-token> (SPEC-CHATBALLS-0013 §7.2):
# статический entry call.html из сборки web-chat, token разбирает браузер.
# Камера/микрофон разрешены только этой странице.
location /calls/ {
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(self), microphone=(self)" always;
try_files /chat/call.html =404;
}
location / {
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header X-Frame-Options SAMEORIGIN always;
try_files $uri $uri/ /index.html;
}
}