mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
Шлюз передавал дальше 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>
99 lines
4.9 KiB
Plaintext
99 lines
4.9 KiB
Plaintext
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;
|
||
}
|
||
}
|