A detailed user guide based on a regular question for correctly configuring Traefik’s ipAllowList with Cloudflare and Tailscale.
The best solution is to use the traefik-get-real-ip plugin to standardize the client IP into the X-Forwarded-For header and then configure ipAllowList to read from that header. This creates a single, unified configuration that works for all your scenarios (Cloudflare, local access, and Tailscale).
Your core issue was correctly identifying that ipAllowList wasn’t using the X-Real-Ip header. The missing link is telling ipAllowList how to find the IP you’ve extracted. The key is to have the plugin overwrite the X-Forwarded-For header and then instruct ipAllowList to use it with ipStrategy.
## The Core Problem: Finding the Real “From” Address
When you use proxies like Cloudflare or networks like Tailscale, your Traefik instance doesn’t see the original user’s IP address directly. It sees the IP of the last proxy that connected to it.
-
Cloudflare: Traefik sees a Cloudflare IP. The real user IP is in a header like
CF-Connecting-IP. -
Tailscale: Traefik sees the IP of the Tailscale client on the Docker network, not the user’s actual device IP within the Tailnet.
-
Local Network: Traefik sees the user’s local IP address.
Your challenge is to make ipAllowList aware of the true source IP, regardless of how the user is connecting.
## The Solution: A Two-Step Strategy
We’ll use a combination of a plugin and middleware configuration to solve this permanently.
-
Step 1: Standardize the Client IP: Use the
traefik-get-real-ipplugin to inspect incoming requests. Based on the connection type (Cloudflare or direct), it will find the true client IP and place it into theX-Forwarded-Forheader. -
Step 2: Tell
ipAllowListWhere to Look: Configure theipAllowListmiddleware to use itsipStrategyto look at theX-Forwarded-Forheader that the plugin just prepared.
## Step 1: Correctly Configure the traefik-get-real-ip Plugin
You were on the right track with this plugin. The key is to configure it to overwrite X-Forwarded-For (OverwriteXFF: true). This makes it the single source of truth for the client IP.
Here is the corrected middleware configuration in your dynamic configuration file (e.g., rules.yml):
middlewares:
realip:
plugin:
traefik-get-real-ip:
# Rule 1: Handle traffic from Cloudflare
Proxy:
- proxyHeadername: "CF-Connecting-IP" # Check if the CF header exists
realIP: "CF-Connecting-IP" # If so, the real IP is in this header
OverwriteXFF: true # CRITICAL: Replace X-Forwarded-For with this IP
# Rule 2: Handle all other traffic (Local, Tailscale, etc.)
- proxyHeadername: "*" # A wildcard for any other request
realIP: "RemoteAddr" # The real IP is the direct connecting IP
OverwriteXFF: true # CRITICAL: Replace X-Forwarded-For with this IP
What this does:
-
If a request comes from Cloudflare, the
CF-Connecting-IPheader will exist. The plugin grabs the user’s IP from it and writes it into theX-Forwarded-Forheader, overwriting anything that was there. -
If a request comes directly (local network or Tailscale), the first rule is skipped. The second wildcard rule matches, takes the IP from the direct connection (
RemoteAddr), and writes it into theX-Forwarded-Forheader.
## Step 2: Configure ipAllowList to Use the Correct IP
Now that the correct client IP is consistently in X-Forwarded-For, you can configure ipAllowList to read it. The secret is setting ipstrategy.depth to 0.
In Traefik, depth: 0 tells the middleware to use the first IP listed in the X-Forwarded-For header, which is exactly the one our plugin just placed there.
YAML
middlewares:
local-only:
ipallowlist:
sourceRange:
- "127.0.0.1/32" # Localhost
- "192.168.1.0/24" # Your local network range
- "YOUR_WAN_IP/32" # Your home's public IP for external access
- "100.64.0.0/10" # Tailscale's CGNAT IP range for all your devices
ipstrategy:
depth: 0 # IMPORTANT: Use the IP our 'realip' plugin prepared
Why depth: 0?
Previous attempts using depth: 1 failed because it assumes a trusted proxy is always in front. By using the plugin to sanitize the IP and setting depth: 0, you create a system that doesn’t need to guess. It simply uses the IP you’ve designated as the “real” one.
## Putting It All Together: A Complete Example
Here is how your static (traefik_config.yml) and dynamic_config (rules.yml) configurations would look.
traefik_config.yml (Static Configuration)
x-trusted-ips: &trustedIPs
# Internal
- 127.0.0.1/32
- 192.168.0.0/16
- 172.16.0.0/12
- 192.168.0.1/24
- 10.0.0.0/8
- 192.168.1.229
- 172.23.0.0/16 # Traefik
# Cloudflare V4
- 173.245.48.0/20
- 103.21.244.0/22
- 103.22.200.0/22
- 103.31.4.0/22
- 141.101.64.0/18
- 108.162.192.0/18
- 190.93.240.0/20
- 188.114.96.0/20
- 197.234.240.0/22
- 198.41.128.0/17
- 162.158.0.0/15
- 104.16.0.0/13
- 104.24.0.0/14
- 172.64.0.0/13
- 131.0.72.0/22
# Cloudflare V6
- 2400:cb00::/32
- 2606:4700::/32
- 2803:f800::/32
- 2405:b500::/32
- 2405:8100::/32
- 2a06:98c0::/29
- 2c0f:f248::/32
api:
dashboard: true
ping:
manualRouting: true
terminatingStatusCode: 503
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
forwardedHeaders:
trustedIPs: *trustedIPs
websecure:
address: ":443"
http:
tls:
options: default
certResolver: letsencrypt
forwardedHeaders:
trustedIPs: *trustedIPs
proxyProtocol:
trustedIPs: *trustedIPs
http3:
address: ":443/udp"
experimental:
plugins:
traefik-get-real-ip:
moduleName: "https://github.com/Paxxs/traefik-get-real-ip"
version: "v1.0.3"
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.0.0"
# ... rest of your static config (providers, etc.)
rules.yml (Dynamic Configuration)
http:
middlewares:
# 1. The Real IP Plugin
realip:
plugin:
traefik-get-real-ip:
Proxy:
- proxyHeadername: "CF-Connecting-IP"
realIP: "CF-Connecting-IP"
OverwriteXFF: true
- proxyHeadername: "*"
realIP: "RemoteAddr"
OverwriteXFF: true
# 2. The IP Allowlist Middleware
local-only:
ipallowlist:
sourceRange:
- "127.0.0.1/32"
- "192.168.1.0/24" #<-- Your local LAN
- "YOUR_WAN_IP/32" #<-- Your public IP
- "100.64.0.0/10" #<-- Tailscale IPs
ipstrategy:
depth: 0
routers:
whoami:
entryPoints:
- "websecure"
rule: "Host(`whoami.domain.com`)"
middlewares:
- realip # First, get the correct IP
- local-only # Then, check it against the allowlist
service: whoami
tls: {}
# ... your services definition
This configuration is robust and solves all your use cases without conflict. You no longer need to switch configs or worry about which DNS server a device is using.
Plugin