Обратный прокси
Панель 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
разрывает соединение.
Даже если 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 выглядит так:
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.
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 и на неизвестные домены отсекает отдельный блок-заглушка:
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.
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.
- Безопасность панели — что уже сделано и о чём помнить.