Skip to main content

nginx

Overview​

nginx is installed in front of the XLADA panel and solves three tasks: it accepts traffic on ports 80 and 443, terminates TLS, and passes panel requests to 127.0.0.1:3000, adding the headers by which the panel understands that the connection is secure and who the real client is.

Proxying is mandatory, not optional: the panel and subscription page code contains middleware that drops the connection if a request arrives without the X-Forwarded-For and X-Forwarded-Proto: https headers. Therefore the panel must already be up before you install nginx — see Panel installation, and for the second host, Subscription page.

warning

You will need a registered domain and access to its DNS records. Without a domain you cannot issue a certificate, and without HTTPS the panel will not work.

DNS: point the domain to the server​

Find out the server's public address. A static IPv4 address is preferable. Then create records at your registrar or DNS provider:

TypeNameValuePurpose
Apanelserver IPv4 addresspanel and API
AAAApanelserver IPv6 addressthe same over IPv6, if available
Asubserver IPv4 addresssubscription page

If the panel should live on the domain itself, enter @ in the name field. Create AAAA records only when IPv6 is actually configured on the server: otherwise clients will go nowhere and get a connection error.

Record updates take from a few minutes to a few hours. You can check the result with:

dig +short panel.example.com

Grey and orange cloud​

In the Cloudflare dashboard, a record has a proxy toggle:

  • grey cloud — the record returns your server's address to the client directly; the certificate is issued and renewed on the server, and IP restrictions work as usual;
  • orange cloud — traffic goes through Cloudflare, and the client sees Cloudflare addresses instead of yours. This hides the server and smooths out spikes, but it requires separate real client IP configuration (see the section below) and does not allow issuing a certificate via port 80 validation without additional conditions.

For the panel, the orange cloud is usually chosen, and trust for the CF-Connecting-IP header must be configured. For details, see the Cloudflare section.

Certificate​

certbot — the main path​

certbot issues a Let's Encrypt certificate and renews it on its own timer. First create a complete HTTP host: it serves domain ownership validation and redirects all other requests to 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
/etc/nginx/sites-available/xlada-bootstrap
# Temporary host for issuing certificates for the panel and the subscription page.
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 will issue a separate certificate for each domain: they will land in /etc/letsencrypt/live/panel.example.com/ and /etc/letsencrypt/live/sub.example.com/. After issuing, delete the temporary host and enable the complete configurations from the sections below: the certificate paths in them are already specified.

sudo rm -f /etc/nginx/sites-enabled/xlada-bootstrap

certbot performs renewal itself. Check it like this:

sudo certbot renew --dry-run
systemctl list-timers | grep certbot

acme.sh — alternative​

If certbot is unavailable or you prefer a single tool for several servers, use acme.sh with ALPN domain validation. This method does not depend on the web server, but requires a free port 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

# One certificate for both domains at once: issue and pin the paths
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"

# Pin the paths and the reload command for automatic renewal
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"

# Bring the file names in line with those specified in the complete configs below
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

Port 8443 must be open and not used by another process. acme.sh renews the certificate itself about every 60 days via cron — make sure the cron service is running.

note

If nginx runs in a container (see below), in --reloadcmd specify a reload inside the container instead of a service reload: docker exec xlada-nginx nginx -s reload.

warning

Keep the certificate authority limitation in mind: the .ru, .su, and .Ҁ҄ domain zones are not supported when issuing via ZeroSSL, which acme.sh uses by default. For such domains, choose certbot or another certificate authority.

nginx configuration for the panel​

Create the virtual host file and enable it:

sudo nano /etc/nginx/sites-available/xlada-panel
sudo ln -sf /etc/nginx/sites-available/xlada-panel /etc/nginx/sites-enabled/
/etc/nginx/sites-available/xlada-panel
# XLADA panel virtual host.

upstream xlada_panel {
server 127.0.0.1:3000;
keepalive 32;
}

# WebSocket needs the upgrade value, regular requests need close.
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;

# Let's Encrypt certificate
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;

# Protocols and ciphers: Mozilla Intermediate recommendations
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;

# Sessions and certificate revocation checking
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;

# Security headers. Enable HSTS only after making sure that HTTPS
# works: the browser will remember this requirement for a long time.
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;

# Request body size: headroom for uploading files through the interface
client_max_body_size 25m;
client_body_timeout 60s;

# Response compression
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 Vite build lives in /assets/ ─────
# File names contain a hash, so they can be cached for a long time.
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;

# Without these headers the panel drops the connection
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 terminal to nodes and live interface updates
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

# The terminal keeps the connection open for hours
proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;

proxy_buffering on;
}
}

A separate stub block closes requests that arrive by IP address or with a wrong server name: no certificate is served to such clients at all.

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

ssl_reject_handshake on;
}

Check the configuration and reload nginx:

sudo nginx -t && sudo systemctl reload nginx

IP allowlist​

The base configuration has no IP-based access control: the panel is protected by its own authentication, and the subscription page must be open to all clients. If the panel is accessible only to you, use the complete config of the same host with an address filter: it is given below and is written to the same file /etc/nginx/sites-available/xlada-panel.

/etc/nginx/sites-available/xlada-panel
# XLADA panel virtual host with an IP allowlist.

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 only for trusted addresses ────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

# Allow only trusted addresses, return 403 to everyone else
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;
}
}

Apply such a filter only to the panel host. On the subscription page host it will break link delivery to users, and on the API it will break webhooks and external integrations. Basic auth as a second layer is inconvenient: it breaks API tokens and service requests, so rely on the administrator password and TOTP instead.

nginx in a container​

If your nginx also runs in Docker, enable host network mode for it: then the container sees the same 127.0.0.1:3000 and 127.0.0.1:3010 as the host, and the complete configurations from this page work without a single edit.

/opt/xlada/nginx/docker-compose.yml
services:
xlada-nginx:
image: nginx:1.30
container_name: xlada-nginx
restart: unless-stopped
# host mode: the upstreams 127.0.0.1:3000 and 127.0.0.1:3010 are reachable as on the host
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

Updating the configuration comes down to editing the files in /etc/nginx/sites-available/ and reloading inside the container:

docker exec xlada-nginx nginx -t && docker exec xlada-nginx nginx -s reload

Subscription page​

The subscription page runs on 127.0.0.1:3010 in the xpanel-subpage container and lives on a separate subdomain. The requirements are the same: HTTPS and the X-Forwarded-For and X-Forwarded-Proto: https headers, otherwise the container drops the connection. WebSocket is not needed here, and the timeout can be shorter.

/etc/nginx/sites-available/xlada-subpage
# XLADA subscription page virtual host.

upstream xlada_subpage {
server 127.0.0.1:3010;
keepalive 16;
}

# The subscription page does not need WebSocket, but the headers are kept:
# that way the config stays working if the application starts using them.
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;

# Subscription page build static files
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;

# Without these headers the subscription page drops the connection
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: not used right now, but the config is ready for it
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;
}
}

Enable the host and reload 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

If CUSTOM_SUB_PREFIX is set in the panel, the same path must be added to SUB_PUBLIC_DOMAIN — see Subscription page for details. And in the subscription page's own environment file, keep TRUST_PROXY=1 so that it trusts one proxy hop and takes the client IP from the header.

Real client IP​

The panel must see the real client address, because the following depend on it:

  • device accounting by HWID — device binding and request history store the request address (Device limit);
  • admin audit log: the address shows who logged in and from where (Panel security);
  • brute-force protection on login: login attempts are counted by address, and if all requests come from a single proxy address, the limit will either trigger for everyone at once or not trigger at all.

If Cloudflare sits in front of nginx, the usual X-Forwarded-For is useless: Cloudflare passes the client address in the CF-Connecting-IP header. This header can be trusted only from Cloudflare subnets — otherwise anyone can put someone else's address into a request.

/etc/nginx/conf.d/real-ip.conf
# Take the client address from the Cloudflare header.
# The list of Cloudflare ranges changes from time to time — verify it.
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;

If your own load balancer or a second proxy sits in front of nginx, trust its subnet and the usual header:

/etc/nginx/conf.d/real-ip.conf
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;
danger

Never specify set_real_ip_from 0.0.0.0/0 and never trust X-Forwarded-For from the entire internet. And do not replace X-Forwarded-Proto with the value http manually: the panel will refuse to work.

Verification​

# 1. Syntax and reload
sudo nginx -t && sudo systemctl reload nginx

# 2. Redirect from HTTP to HTTPS — expect 301
curl -sI http://panel.example.com | head -1

# 3. HTTPS: response code and HSTS header
curl -sI https://panel.example.com | grep -Ei 'HTTP/|strict-transport'

# 4. Certificate expiration date
echo | openssl s_client -connect panel.example.com:443 \
-servername panel.example.com 2>/dev/null | openssl x509 -noout -dates

# 5. Subscription page — expect 200 and the profile-web-page-url header
curl -sI https://sub.example.com/AbCdEf1234567890

What to look at if something does not work:

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

Typical signs: 502 Bad Gateway — the panel container is not running or listens on the wrong port; TLS handshake errors — a wrong certificate path or an expired certificate; a connection drop right after the request — the X-Forwarded-For or X-Forwarded-Proto headers are lost. In the access log, the client address must match its real address, not the Cloudflare address.

What's next​