nginx
Обзор
nginx ставится перед панелью XLADA и решает три задачи: принимает трафик на
портах 80 и 443, завершает TLS и передаёт запросы панели на 127.0.0.1:3000,
подставляя заголовки, по которым панель понимает, что соединение защищено и кто
настоящий клиент.
Проксирование обязательно, а не желательно: в коде панели и страницы подписки
есть middleware, который разрывает соединение, если запрос пришёл без
заголовков X-Forwarded-For и X-Forwarded-Proto: https. Поэтому перед
установкой nginx панель уже должна быть поднята — см.
Установка панели, а для второго хоста —
Страница подписки.
Вам понадобится зарегистрированный домен и доступ к его DNS-записям. Без домена сертификат не выпустить, а без HTTPS панель работать не будет.
DNS: направляем домен на сервер
Узнайте внешний адрес сервера. Лучше, если это постоянный IPv4-адрес. Затем создайте записи у регистратора или у DNS-провайдера:
| Тип | Имя | Значение | Назначение |
|---|---|---|---|
A | panel | IPv4-адрес сервера | панель и API |
AAAA | panel | IPv6-адрес сервера | то же по IPv6, если он есть |
A | sub | IPv4-адрес сервера | страница подписки |
Если панель должна жить на самом домене, укажите в поле имени @. Записи
AAAA создавайте только тогда, когда IPv6 действительно настроен на сервере:
иначе клиенты уйдут в никуда и получат ошибку соединения.
Обновление записей занимает от нескольких минут до нескольких часов. Проверить результат можно командой:
dig +short panel.example.com
Серое и оранжевое облако
В панели управления Cloudflare у записи есть переключатель проксирования:
- серое облако — запись отдаёт клиенту адрес вашего сервера напрямую; сертификат выпускается и продлевается на сервере, ограничения по IP работают как обычно;
- оранжевое облако — трафик идёт через Cloudflare, клиент видит адреса Cloudflare, а не ваш. Это скрывает сервер и сглаживает всплески, но требует отдельной настройки реального IP клиента (см. раздел ниже) и не даёт выпускать сертификат проверкой по порту 80 без дополнительных условий.
Для панели обычно выбирают оранжевое облако и обязательно настраивают доверие к
заголовку CF-Connecting-IP. Подробности — в разделе
Cloudflare.
Сертификат
certbot — основной путь
certbot выпускает сертификат Let's Encrypt и продлевает его по своему таймеру. Сначала создайте полный HTTP-хост: он отдаёт проверку владения доменом и перенаправляет остальные запросы на HTTPS.
sudo mkdir -p /var/www/certbot
sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo nano /etc/nginx/sites-available/xlada-bootstrap
# Временный хост для выпуска сертификатов панели и страницы подписки.
server {
listen 80;
listen [::]:80;
server_name panel.example.com sub.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
sudo ln -sf /etc/nginx/sites-available/xlada-bootstrap /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d panel.example.com
sudo certbot --nginx -d sub.example.com
Certbot выпустит отдельный сертификат для каждого домена: они лягут в
/etc/letsencrypt/live/panel.example.com/ и
/etc/letsencrypt/live/sub.example.com/. После выпуска удалите временный хост и
включите полные конфигурации из разделов ниже: пути к сертификатам в них уже
указаны.
sudo rm -f /etc/nginx/sites-enabled/xlada-bootstrap
Продление certbot выполняет сам. Проверьте его так:
sudo certbot renew --dry-run
systemctl list-timers | grep certbot
acme.sh — альтернатива
Если certbot недоступен или вы предпочитаете один инструмент для нескольких серверов, используйте acme.sh с проверкой владения доменом по ALPN. Этот способ не зависит от веб-сервера, но требует свободного порта 8443.
sudo apt-get install -y cron socat
curl https://get.acme.sh | sh -s email=admin@example.com
source ~/.bashrc
sudo mkdir -p /opt/xlada/nginx && cd /opt/xlada/nginx
# Один сертификат сразу на оба домена: выпуск и закрепление путей
acme.sh --issue --standalone -d 'panel.example.com' -d 'sub.example.com' \
--key-file /opt/xlada/nginx/privkey.key \
--fullchain-file /opt/xlada/nginx/fullchain.pem \
--alpn --tlsport 8443 \
--reloadcmd "systemctl reload nginx"
# Закрепление путей и команды перезагрузки для автопродления
acme.sh --install-cert -d 'panel.example.com' \
--key-file /opt/xlada/nginx/privkey.key \
--fullchain-file /opt/xlada/nginx/fullchain.pem \
--reloadcmd "systemctl reload nginx"
# Приводим имена файлов к тем, что указаны в полных конфигурациях ниже
sudo mkdir -p /etc/letsencrypt/live/panel.example.com
sudo mkdir -p /etc/letsencrypt/live/sub.example.com
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/panel.example.com/fullchain.pem
sudo ln -sf /opt/xlada/nginx/privkey.key /etc/letsencrypt/live/panel.example.com/privkey.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/panel.example.com/chain.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/sub.example.com/fullchain.pem
sudo ln -sf /opt/xlada/nginx/privkey.key /etc/letsencrypt/live/sub.example.com/privkey.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/sub.example.com/chain.pem
Порт 8443 должен быть открыт и не занят другим процессом. acme.sh продлевает сертификат сам примерно каждые 60 дней через cron — проверьте, что служба cron запущена.
Если nginx работает в контейнере (см. ниже), в --reloadcmd вместо перезагрузки
службы укажите перезагрузку внутри контейнера:
docker exec xlada-nginx nginx -s reload.
Учтите ограничение центра сертификации: доменные зоны .ru, .su и .рф не
поддерживаются при выпуске через ZeroSSL, который acme.sh использует по
умолчанию. Для таких доменов выбирайте certbot или другой центр сертификации.
Конфигурация nginx для панели
Создайте файл виртуального хоста и включите его:
sudo nano /etc/nginx/sites-available/xlada-panel
sudo ln -sf /etc/nginx/sites-available/xlada-panel /etc/nginx/sites-enabled/
# Виртуальный хост панели XLADA.
upstream xlada_panel {
server 127.0.0.1:3000;
keepalive 32;
}
# Для WebSocket нужно значение upgrade, для обычных запросов — close.
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;
# Сертификат Let's Encrypt
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;
# Протоколы и шифры: рекомендации Mozilla Intermediate
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;
# Заголовки безопасности. HSTS включайте, только убедившись, что HTTPS
# работает: браузер запомнит это требование надолго.
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 25m;
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;
# ─── Статика интерфейса: сборка Vite лежит в /assets/ ─────────────
# Имена файлов содержат хеш, поэтому их можно кэшировать надолго.
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;
# WebSocket: SSH-терминал к нодам и живые обновления интерфейса
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 nginx -t && sudo systemctl reload nginx
Ограничение доступа по IP
В базовой конфигурации доступа по IP нет: панель защищена собственной
аутентификацией, а страница подписки должна быть открыта всем клиентам. Если
панель доступна только вам, используйте полный конфиг того же хоста с фильтром
адресов: он приведён ниже и записывается в тот же файл
/etc/nginx/sites-available/xlada-panel.
# Виртуальный хост панели XLADA с ограничением доступа по IP.
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;
# Пускаем только доверенные адреса, остальным — 403
allow 203.0.113.0/24;
allow 198.51.100.7;
deny all;
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 25m;
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;
}
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;
}
}
Такой фильтр ставьте только на хост панели. На хосте страницы подписки он сломает выдачу ссылок пользователям, а на API — вебхуки и внешние интеграции. Basic auth как второй слой неудобен: он ломает работу API-токенов и служебных запросов, поэтому вместо него полагайтесь на пароль администратора и TOTP.
nginx в контейнере
Если nginx у вас тоже в Docker, включите для него host-режим сети: тогда
контейнер видит те же 127.0.0.1:3000 и 127.0.0.1:3010, что и хост, и полные
конфигурации из этой страницы подходят без единой правки.
services:
xlada-nginx:
image: nginx:1.30
container_name: xlada-nginx
restart: unless-stopped
# host-режим: апстримы 127.0.0.1:3000 и 127.0.0.1:3010 доступны как на хосте
network_mode: host
volumes:
- /etc/nginx/sites-available/xlada-panel:/etc/nginx/conf.d/xlada-panel.conf:ro
- /etc/nginx/sites-available/xlada-subpage:/etc/nginx/conf.d/xlada-subpage.conf:ro
- /etc/nginx/conf.d/00-default.conf:/etc/nginx/conf.d/00-default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot:ro
cd /opt/xlada/nginx
docker compose up -d
docker exec xlada-nginx nginx -t
docker exec xlada-nginx nginx -s reload
Обновление конфигурации сводится к правке файлов в /etc/nginx/sites-available/
и перезагрузке внутри контейнера:
docker exec xlada-nginx nginx -t && docker exec xlada-nginx nginx -s reload
Страница подписки
Страница подписки работает на 127.0.0.1:3010 в контейнере xpanel-subpage и
живёт на отдельном поддомене. Требования те же: HTTPS и заголовки
X-Forwarded-For и X-Forwarded-Proto: https, иначе контейнер разрывает
соединение. WebSocket здесь не нужен, а таймаут можно сделать короче.
# Виртуальный хост страницы подписки XLADA.
upstream xlada_subpage {
server 127.0.0.1:3010;
keepalive 16;
}
# Странице подписки WebSocket не нужен, но заголовки оставлены:
# так конфиг остаётся рабочим, если приложение начнёт их использовать.
map $http_upgrade $subpage_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name sub.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 sub.example.com;
ssl_certificate /etc/letsencrypt/live/sub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sub.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/sub.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 8.8.8.8 valid=60s;
resolver_timeout 2s;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
client_max_body_size 10m;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
application/javascript
application/json
application/manifest+json
application/xml
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
# Статика сборки страницы подписки
location ^~ /assets/ {
proxy_pass http://xlada_subpage;
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;
}
location / {
proxy_pass http://xlada_subpage;
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;
# WebSocket: сейчас не используется, но конфиг готов к нему
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $subpage_connection_upgrade;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffering on;
}
}
Включите хост и перезагрузите nginx:
sudo ln -sf /etc/nginx/sites-available/xlada-subpage /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run
Если в панели задан CUSTOM_SUB_PREFIX, тот же путь нужно добавить в
SUB_PUBLIC_DOMAIN — подробности в разделе
Страница подписки. А в файле окружения самой
страницы оставьте TRUST_PROXY=1, чтобы она доверяла одному хопу прокси и
брала IP клиента из заголовка.
Реальный IP клиента
Панель должна видеть настоящий адрес клиента, потому что от него зависят:
- учёт устройств по HWID — привязка устройства и история обращений хранят адрес запроса (Лимит устройств);
- журнал действий администратора: по адресу видно, кто и откуда вошёл (Безопасность панели);
- защита входа от перебора: попытки входа считаются по адресу, и если все запросы приходят с одного адреса прокси, ограничение либо сработает на всех сразу, либо не сработает вовсе.
Если перед nginx стоит Cloudflare, обычный X-Forwarded-For бесполезен:
Cloudflare передаёт адрес клиента в заголовке CF-Connecting-IP. Доверять этому
заголовку можно только от подсетей Cloudflare — иначе любой желающий подставит
в запрос чужой адрес.
# Адрес клиента берём из заголовка Cloudflare.
# Список диапазонов Cloudflare время от времени меняется — сверяйтесь с ним.
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;
Если перед nginx стоит ваш собственный балансировщик или второй прокси, доверяйте его подсети и обычному заголовку:
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Никогда не указывайте set_real_ip_from 0.0.0.0/0 и не доверяйте
X-Forwarded-For от всего интернета. И не подменяйте X-Forwarded-Proto
значением http вручную: панель откажется работать.
Проверка
# 1. Синтаксис и перезагрузка
sudo nginx -t && sudo systemctl reload nginx
# 2. Редирект с HTTP на HTTPS — ожидаем 301
curl -sI http://panel.example.com | head -1
# 3. HTTPS: код ответа и заголовок HSTS
curl -sI https://panel.example.com | grep -Ei 'HTTP/|strict-transport'
# 4. Срок действия сертификата
echo | openssl s_client -connect panel.example.com:443 \
-servername panel.example.com 2>/dev/null | openssl x509 -noout -dates
# 5. Страница подписки — ожидаем 200 и заголовок profile-web-page-url
curl -sI https://sub.example.com/AbCdEf1234567890
Что смотреть, если что-то не работает:
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
sudo journalctl -u nginx -n 50 --no-pager
docker compose logs --tail 50 xpanel
Типичные признаки: 502 Bad Gateway — контейнер панели не запущен или слушает
не тот порт; ошибки рукопожатия TLS — неверный путь к сертификату или истёкший
срок; обрыв соединения сразу после запроса — потеряны заголовки
X-Forwarded-For или X-Forwarded-Proto. В логе доступа адрес клиента должен
совпадать с его настоящим адресом, а не с адресом Cloudflare.