Перейти к основному содержимому

Обратный прокси

Панель XLADA не работает без обратного прокси с TLS. В её коде есть middleware, который разрывает соединение, если запрос пришёл без заголовков X-Forwarded-For и X-Forwarded-Proto: https. Это не прихоть: панель должна быть уверена, что наружу её отдаёт доверенный узел, иначе определить реальный IP клиента невозможно.

Выберите веб-сервер — в каждом разделе полный рабочий конфиг, а не минимальный пример:

ВариантКогда выбирать
nginxклассика, максимальный контроль над каждым заголовком и таймаутом
Caddyминимум конфигурации, сертификаты выпускаются автоматически
Angieфорк nginx с готовыми модулями; знакомый синтаксис
Traefikу вас уже есть Docker-стек и хочется описывать маршруты labels'ами
Cloudflare Tunnelу сервера нет белого IP или origin нужно скрыть

Все варианты решают одну задачу: принять HTTPS снаружи, добавить заголовки и передать запрос на 127.0.0.1:3000 (панель) или 127.0.0.1:3010 (страница подписки). Панель, база и Valkey слушают только localhost — наружу их не выставляют.

Что настроить в любом случае​

Перечисленные ниже вещи есть в каждом полном конфиге на страницах раздела. Здесь — что именно проверить, если вы правите конфигурацию под себя.

1. Заголовки прокси​

Панель и страница подписки читают адрес клиента из X-Forwarded-For, а признак HTTPS — из X-Forwarded-Proto. Оба заголовка обязательны: без них middleware разрывает соединение.

Не переопределяйте X-Forwarded-Proto

Даже если TLS терминируется на балансировщике выше, не подставляйте сюда http вручную: панель откажется работать.

2. WebSocket​

Из панели открывается SSH-терминал к нодам — это WebSocket. Заголовок Connection подставляется только тогда, когда клиент просит обновление протокола, поэтому в nginx и Angie его вычисляют через map $http_upgrade $connection_upgrade. Без проброса заголовков Upgrade и Connection терминал молча не подключится.

3. Реальный IP клиента​

От корректного IP зависят три вещи в XLADA:

  • лимит устройств (HWID) — считается по запросам подписки;
  • журнал действий администратора — фиксирует IP;
  • защита входа от перебора — блокировки считаются по паре «логин + IP».

Если перед веб-сервером стоит CDN или второй прокси, настройте доверие к его заголовкам. Полный файл доверия к подсетям Cloudflare для nginx выглядит так:

/etc/nginx/conf.d/real-ip.conf
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

real_ip_header CF-Connecting-IP;
real_ip_recursive on;

У Caddy за это отвечает trusted_proxies static, у Traefik — forwardedHeaders.trustedIPs, у Cloudflare Tunnel — собственная схема с локальным прокси. Иначе все клиенты будут выглядеть как один IP, и лимиты сработают не так, как вы ожидаете.

4. Таймауты, стриминг и кэш​

  • Долгие соединения. SSH-терминал и стриминговые ответы API живут минутами: ставьте proxy_read_timeout не меньше 300 секунд, для терминала — сутки.
  • Отключение буферизации. Для потоковых эндпоинтов полезно proxy_buffering off, иначе ответ копится в буфере и приходит рывками.
  • Размер тела запроса. Импорт конфигураций и шаблонов бывает крупным — client_max_body_size 32m снимает неожиданные 413.
  • Кэш статики. Сборка интерфейса лежит в /assets/ и содержит хеш в имени файла, поэтому её отдают с заголовком Cache-Control: public, immutable и большим сроком жизни.

Полный конфиг nginx​

Ниже — полный рабочий конфиг для панели: сертификат, TLS, заголовки, WebSocket, таймауты, gzip и кэш статики. Отдельная страница nginx дополняет его хостом страницы подписки, ограничением доступа по IP и запуском в Docker.

/etc/nginx/sites-available/xlada-panel
upstream xlada_panel {
server 127.0.0.1:3000;
keepalive 32;
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# HTTP: проверка владения доменом и редирект на HTTPS
server {
listen 80;
listen [::]:80;
server_name panel.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

# HTTPS
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/panel.example.com/chain.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=60s;
resolver_timeout 2s;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

client_max_body_size 32m;
client_body_timeout 60s;

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/wasm
application/xhtml+xml
application/xml
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

# Статика интерфейса: хеш в имени файла позволяет кэшировать её надолго
location ^~ /assets/ {
proxy_pass http://xlada_panel;
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 Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

# API и потоковые ответы: без буферизации
location /api/ {
proxy_pass http://xlada_panel;
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 $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}

# Интерфейс панели
location / {
proxy_pass http://xlada_panel;
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 $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;

proxy_buffering on;
}
}

Запросы по IP и на неизвестные домены отсекает отдельный блок-заглушка:

/etc/nginx/conf.d/00-default.conf
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;

ssl_reject_handshake on;
}

Включите хост и перезагрузите nginx:

sudo ln -sf /etc/nginx/sites-available/xlada-panel /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Сертификат для этого конфига выпускает certbot — полная последовательность описана на странице nginx.

Сокращённый вариант​

Если нужен минимум для теста, подойдёт такой же законченный, но короткий конфиг: те же редирект, TLS, заголовки и WebSocket, без gzip, кэша статики и отдельного блока для API.

/etc/nginx/sites-available/xlada-panel
server {
listen 80;
listen [::]:80;
server_name panel.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;

client_max_body_size 32m;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

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 86400;
}
}

Проверка​

nginx -t && systemctl reload nginx

# Панель отвечает и видит HTTPS
curl -sI https://panel.example.com | head -3
curl -s https://panel.example.com/api/system/health

# В логах нет «Reverse proxy and HTTPS are required»
tail -n 50 /var/log/nginx/error.log

Чек-лист​

  • HTTP редиректит на HTTPS, сертификат продлевается автоматически.
  • X-Forwarded-For и X-Forwarded-Proto: https доходят до панели.
  • WebSocket работает: SSH-терминал к ноде открывается.
  • Реальный IP клиента виден в журнале действий администратора.
  • Панель и страница подписки отдаются разными доменами (если страница подписки включена).
  • Порты 3000, 3001 и 3010 закрыты наружу — доступны только с localhost.

Что дальше​

  • nginx — полный конфиг с TLS, gzip, кэшем и security headers.
  • Caddy — сертификаты «из коробки».
  • Traefik — маршруты labels'ами и ограничение по IP.
  • Безопасность панели — что уже сделано и о чём помнить.