n8n Behind Cloudflare + OPNsense HAProxy (Unraid Backend)
Step-by-step topology for exposing n8n through Cloudflare, OPNsense HAProxy, and Unraid with the right headers, WebSocket support, and TLS expectations.
n8n behind Cloudflare + OPNsense HAProxy (Unraid backend)
Expose
n8n
at https://n8n.example.com through Cloudflare → OPNsense/HAProxy → Unraid (Docker) with correct headers and WebSocket support. Avoids common pitfalls that cause CSRF “Origin header does NOT match…” and HTTP 503.
Topology
Figure 1: n8n Network Architecture - Cloudflare → OPNsense → Unraid
Browser ──HTTPS──► Cloudflare (proxy: orange cloud)
│
▼
OPNsense (HAProxy, TLS offload @ :443)
│
▼
Unraid (Docker) → n8n container (HTTP :5678)
🔧 Key Components
- Cloudflare: DNS proxy with DDoS protection and SSL termination
- OPNsense + HAProxy: Load balancer with TLS offloading
- Unraid Docker: Containerized n8n instance
Prerequisites
- Cloudflare DNS:
n8n.example.com
→ WAN IP of OPNsense (proxied, orange cloud). - OPNsense with HAProxy plugin.
- Unraid Docker n8n container reachable on LAN (e.g.,
10.0.3.129:5678
). - Valid TLS cert on OPNsense (e.g.,
*.example.com
via ACME client).
1) n8n container (Unraid) — required environment variables
Set these on the n8n container and recreate/apply:
N8N_HOST = n8n.example.com
N8N_PORT = 443
N8N_PROTOCOL = https
N8N_EDITOR_BASE_URL = https://n8n.example.com/
WEBHOOK_URL = https://n8n.example.com/
N8N_SECURE_COOKIE = true
Optional (noise reduction):
N8N_DIAGNOSTICS_ENABLED = false
N8N_DISABLE_VERSION_NOTIFICATIONS = true
Verify inside the running container:
docker exec -it n8n env | grep -E 'N8N_|WEBHOOK_URL'
If you ever saw “Expected: …” in logs, once envs are correct do this once:
docker stop n8n
rm -f /mnt/user/appdata/n8n/config
docker start n8n
Then hard-refresh your browser (Ctrl+F5 / Cmd+Shift+R).
2) OPNsense HAProxy — Backend (to n8n)
Real Server
- Name:
n8n_server
- IP/FQDN:
10.0.3.129
- Port:
5678
- SSL: OFF (backend is plain HTTP)
- Mode: active
Backend Pool
- Name:
n8n_backend
- Mode: HTTP
- Servers:
n8n_server
- Health check: HTTP GET
/
, expect 200–399 - HTTP(S) settings: enable X-Forwarded-For
- Persistence: None
Sanity from LAN:
curl -sS http://10.0.3.129:5678/ | head -n 5
3) OPNsense HAProxy — Conditions
Create in Rules & Checks → Conditions:
host_n8n
→ Host matches →n8n.example.com
is_websocket_upgrade
→ HTTP request header matches → HeaderUpgrade
equalswebsocket
(case-insensitive)
4) OPNsense HAProxy — Rules
Create in Rules & Checks → Rules:
set_xfp_https
→http-request header set
X-Forwarded-Proto =https
set_xfh_host
→http-request header set X-Forwarded-Host %[req.hdr(host)]
ws_upgrade
(conditionis_websocket_upgrade
) →http-request header set Upgrade websocket
ws_connection_upgrade
(conditionis_websocket_upgrade
) →http-request header set Connection upgrade
route_to_n8n
(conditionhost_n8n
) → Use specified Backend Pool →n8n_backend
- (Port 80 only)
redirect_to_https_all
→http-request redirect https://%[req.hdr(host)]%[req.uri]
(code 301)
5) OPNsense HAProxy — Public Services (listeners)
Keep only one listener bound to 0.0.0.0:443. Disable duplicates to avoid conflicts.
Port 80 — redirect only
- Name:
public_facing_pool_80
- Listen:
0.0.0.0:80
- Default Backend: None
- Rules:
redirect_to_https_all
(only this) - Basic Auth / Prometheus: Off
Port 443 — TLS offload + routing
- Name:
public_facing_pool_443
- Listen:
0.0.0.0:443
- SSL offloading: On, certificate
*.example.com
- Client Certificate Auth: Off (unless you require mTLS)
- Basic Authentication: Off (unless intentional)
- Rules attached:
set_xfp_https
set_xfh_host
ws_upgrade
ws_connection_upgrade
route_to_n8n
(Host →n8n_backend
)
Debug tip: If you get a 503, temporarily set Default Backend = n8n_backend
on 443 to prove routing, then switch back to None once host routing works.
6) Cloudflare
- DNS:
n8n.example.com
→ WAN IP of OPNsense (proxied). - SSL/TLS mode: Full (strict) (not Flexible).
- WebSockets: supported automatically over 443.
7) Verification
# 80 should redirect to 443
curl -I http://n8n.example.com
# → HTTP/1.1 301 Moved Permanently … Location: https://n8n.example.com/...
# 443 should succeed (200 / login page), not 503
curl -I https://n8n.example.com
# Backend from LAN
curl -sS http://10.0.3.129:5678/ | head -n 5
8) Troubleshooting
-
503 on HTTPS
- Multiple 443 listeners on
0.0.0.0:443
(disable extras). - Backend wrong/unreachable.
- Missing host-route rule on 443.
- Multiple 443 listeners on
-
“Origin header does NOT match the expected origin”
- Missing/misspelled n8n env vars (verify
N8N_*
andWEBHOOK_URL
). - Tiny settings file overriding envs → remove once (see step 1).
- Browser cache: hard-refresh / incognito.
- Missing/misspelled n8n env vars (verify
-
WebSocket issues
- Missing HAProxy rules to set
Upgrade: websocket
andConnection: upgrade
whenUpgrade: websocket
.
- Missing HAProxy rules to set
9) Optional hardening
- HSTS on 443:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- Add rate limiting/stick tables as needed.
10) Rule quick reference
- redirect_to_https_all →
http-request redirect https://%[req.hdr(host)]%[req.uri]
(301) - set_xfp_https →
http-request header set X-Forwarded-Proto https
- set_xfh_host →
http-request header set X-Forwarded-Host %[req.hdr(host)]
- ws_upgrade (if
Upgrade: websocket
) →http-request header set Upgrade websocket
- ws_connection_upgrade (if
Upgrade: websocket
) →http-request header set Connection upgrade
- route_to_n8n (if Host
n8n.example.com
) → Use backend:n8n_backend