Skip to main content

Reverse Proxy

The XLADA panel does not work without a reverse proxy with TLS. Its code contains middleware that drops the connection if a request arrives without the X-Forwarded-For and X-Forwarded-Proto: https headers. This is not a whim: the panel must be sure that a trusted node serves it to the outside, otherwise the real client IP cannot be determined.

Choose a web server — each section has a complete working config, not a minimal example:

OptionWhen to choose
nginxthe classic choice, maximum control over every header and timeout
Caddyminimal configuration, certificates are issued automatically
Angiean nginx fork with ready-made modules; familiar syntax
Traefikyou already have a Docker stack and want to describe routes with labels
Cloudflare Tunnelthe server has no public IP or the origin must be hidden

All options solve the same task: accept HTTPS from the outside, add headers, and pass the request to 127.0.0.1:3000 (panel) or 127.0.0.1:3010 (subscription page). The panel, database, and Valkey listen only on localhost — they are never exposed to the outside.

What to configure in any case​

The items listed below are present in every complete config on the pages of this section. Here is what exactly to check if you adjust the configuration to your needs.

1. Proxy headers​

The panel and the subscription page read the client address from X-Forwarded-For, and the HTTPS flag from X-Forwarded-Proto. Both headers are mandatory: without them the middleware drops the connection.

Do not override X-Forwarded-Proto

Even if TLS terminates on a load balancer above, do not put http here manually: the panel will refuse to work.

2. WebSocket​

The panel opens an SSH terminal to nodes — this is a WebSocket. The Connection header is added only when the client requests a protocol upgrade, so in nginx and Angie it is computed via map $http_upgrade $connection_upgrade. Without forwarding the Upgrade and Connection headers, the terminal silently fails to connect.

3. Real client IP​

Three things in XLADA depend on a correct IP:

  • the device limit (HWID) — counted from subscription requests;
  • the admin audit log — records the IP;
  • brute-force protection on login — lockouts are counted per "login + IP" pair.

If a CDN or a second proxy sits in front of the web server, configure trust for its headers. The complete Cloudflare subnet trust file for nginx looks like this:

/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;

In Caddy this is handled by trusted_proxies static, in Traefik by forwardedHeaders.trustedIPs, and in Cloudflare Tunnel by its own scheme with a local proxy. Otherwise all clients will look like a single IP, and the limits will not behave as you expect.

4. Timeouts, streaming, and cache​

  • Long-lived connections. The SSH terminal and streaming API responses live for minutes: set proxy_read_timeout to at least 300 seconds, and to a day for the terminal.
  • Disabling buffering. For streaming endpoints, proxy_buffering off is useful; otherwise the response accumulates in the buffer and arrives in bursts.
  • Request body size. Configuration and template imports can be large — client_max_body_size 32m removes unexpected 413 errors.
  • Static cache. The interface build lives in /assets/ and contains a hash in the file name, so it is served with the Cache-Control: public, immutable header and a long lifetime.

Complete nginx config​

Below is a complete working config for the panel: certificate, TLS, headers, WebSocket, timeouts, gzip, and static cache. The separate nginx page extends it with a subscription page host, an IP allowlist, and running in 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: domain ownership validation and redirect to 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;

# Interface static files: the hash in the file name allows long caching
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 and streaming responses: no buffering
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;
}

# Panel interface
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;
}
}

A separate stub block rejects requests by IP and to unknown domains:

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

ssl_reject_handshake on;
}

Enable the host and reload nginx:

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

certbot issues the certificate for this config — the full sequence is described on the nginx page.

Short variant​

If you need the minimum for a test, an equally complete but short config will do: the same redirect, TLS, headers, and WebSocket, without gzip, static cache, and a separate block for the 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;
}
}

Verification​

nginx -t && systemctl reload nginx

# The panel responds and sees HTTPS
curl -sI https://panel.example.com | head -3
curl -s https://panel.example.com/api/system/health

# There is no "Reverse proxy and HTTPS are required" in the logs
tail -n 50 /var/log/nginx/error.log

Checklist​

  • HTTP redirects to HTTPS, the certificate renews automatically.
  • X-Forwarded-For and X-Forwarded-Proto: https reach the panel.
  • WebSocket works: the SSH terminal to a node opens.
  • The real client IP is visible in the admin audit log.
  • The panel and the subscription page are served by different domains (if the subscription page is enabled).
  • Ports 3000, 3001, and 3010 are closed to the outside — reachable only from localhost.

What's next​

  • nginx — the full config with TLS, gzip, cache, and security headers.
  • Caddy — certificates "out of the box".
  • Traefik — routes via labels and IP restrictions.
  • Panel security — what is already done and what to keep in mind.