# Caddy на VPS (186.246.51.17) — единственная публичная точка входа. # Два домена, ОБА с твоими сертификатами; проксируют в SSH-туннели: # # forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD # forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV # # Caddy сам терминирует TLS (он и есть edge: видит реального клиента), а вниз к # приложению передаёт X-Forwarded-Proto=https / X-Forwarded-For / Host — # приложение это учитывает (uvicorn --proxy-headers). Положи файл в /etc/caddy/Caddyfile. # # X-Forwarded-For ПЕРЕЗАПИСЫВАЕМ реальным пиром (header_up ... {remote_host}), а не # добавляем: иначе клиент мог бы подставить своё левое значение и подменить IP для # throttle и аудита (#58). Вместе с сужением forwarded-allow-ips в entrypoint.sh это # делает клиентский IP достоверным. # Сертификаты — см. deploy/vps/README.md (fullchain = leaf + промежуточные одним файлом). # # SSE (/api/events): отдельный handle БЕЗ encode и с flush_interval -1 — иначе сжатие/ # буферизация задержат поток событий (реал-тайм перестанет работать). # # Туннель лёг (апстрим недоступен) → reverse_proxy отдаёт 502/503/504, и сниппет (edge) # подменяет ответ на страницу-заглушку (/etc/caddy/maintenance/maintenance.html) вместо # «зависшей загрузки» SPA. Тот же сниппет запрещает кэш HTML-документа (Cache-Control: no-store), # иначе браузер отдаёт старый index.html из кэша и до Caddy/заглушки запрос не доходит. # Файл заглушки — deploy/vps/maintenance.html. # Edge-поведение, общее для сайтов: security-заголовки + запрет кэша HTML + заглушка при падении апстрима. (edge) { # Security-заголовки (#61). HSTS — принудительный HTTPS на год с поддоменами; nosniff — # запрет MIME-sniffing; frame DENY — защита от кликджекинга (наши страницы нельзя встроить # в чужой iframe); Referrer/Permissions — минимизация утечек. Server скрываем, чтобы не # светить используемый прокси. header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "geolocation=(), microphone=(), camera=()" -Server } # Content-Security-Policy подготовлена, но ВЫКЛЮЧЕНА до проверки: строгая политика легко # ломает SPA (инлайновые стили Vite), Telegram-виджет входа (скрипт с telegram.org + iframe # oauth.telegram.org) и EventSource (/api/events). Раскомментировать после проверки, # что вход и реал-тайм работают (#61). # header Content-Security-Policy "default-src 'self'; script-src 'self' https://telegram.org https://oauth.telegram.org; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-src https://oauth.telegram.org; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" # HTML-документ (навигации, Accept: text/html) НЕ кэшируем. Иначе браузер отдаёт старый # SPA из кэша без сетевого запроса → запрос не доходит до Caddy и заглушку не видно. # Хэшированные ассеты (JS/CSS) под это не попадают (у них другой Accept) и кэшируются как обычно. @html header Accept *text/html* header @html Cache-Control "no-store" # Туннель лёг (апстрим 502/503/504) → отдаём свою страницу-заглушку со статусом 503. handle_errors { @down expression `{http.error.status_code} in [502, 503, 504]` handle @down { root * /etc/caddy/maintenance rewrite * /maintenance.html header Cache-Control "no-store" file_server { status 503 } } } } forbiddenstars.ru { tls /etc/caddy/certs/forbiddenstars.ru/fullchain.pem /etc/caddy/certs/forbiddenstars.ru/privkey.pem @sse path /api/events handle @sse { reverse_proxy 127.0.0.1:9000 { flush_interval -1 header_up X-Forwarded-For {remote_host} } } handle { encode zstd gzip reverse_proxy 127.0.0.1:9000 { header_up X-Forwarded-For {remote_host} } } import edge } forbidden-stars.ru { tls /etc/caddy/certs/forbidden-stars.ru/fullchain.pem /etc/caddy/certs/forbidden-stars.ru/privkey.pem @sse path /api/events handle @sse { reverse_proxy 127.0.0.1:9001 { flush_interval -1 header_up X-Forwarded-For {remote_host} } } handle { encode zstd gzip reverse_proxy 127.0.0.1:9001 { header_up X-Forwarded-For {remote_host} } } import edge }