Solving the Real IP Problem: A Unified ipAllowList Strategy for Traefik, Cloudflare, and Tailscale

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.

  1. Step 1: Standardize the Client IP: Use the traefik-get-real-ip plugin to inspect incoming requests. Based on the connection type (Cloudflare or direct), it will find the true client IP and place it into the X-Forwarded-For header.

  2. Step 2: Tell ipAllowList Where to Look: Configure the ipAllowList middleware to use its ipStrategy to look at the X-Forwarded-For header 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-IP header will exist. The plugin grabs the user’s IP from it and writes it into the X-Forwarded-For header, 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 the X-Forwarded-For header.


## 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

2 Likes

Nice guide! The guide isn’t complete for docker scenarios, i.e. when you have traefik running in a bridged network (e.g. together with Pangolin). Only in host network mode you will see client IPs otherwise you will always see the dockers gateway IP address.

1 Like

Thanks for pointing that out, You arere absolutely right — in a plain bridged network setup, traefik will only ever see the docker gateway IP unless you’re in host mode. That’s exactly why in the guide I leaned on the traefik-get-real-ip plugin + ipStrategy combo: it normalizes the client IP into X-Forwarded-For regardless of whether you’re behind Cloudflare, Pangolin, or just a docker bridge.

So even if you don’t want to run traefik in host mode, you can still recover the real client IP by letting the plugin overwrite XFF and then telling ipAllowList to trust that header. That way the middleware logic works consistently across host, bridge, and overlay networks without having to compromise on isolation.


here’s a concise Docker bridge mode
It shows how to recover the real client IP even when traefik is running in a bridged network (e.g alongside Pangolin), where you would otherwise only see the docker gateway IP:

http:
  middlewares:
    # Step 1: Normalize the client IP
    realip:
      plugin:
        traefik-get-real-ip:
          Proxy:
            # Cloudflare traffic
            - proxyHeadername: "CF-Connecting-IP"
              realIP: "CF-Connecting-IP"
              OverwriteXFF: true
            # Everything else (local LAN, Pangolin, bridge)
            - proxyHeadername: "*"
              realIP: "RemoteAddr"
              OverwriteXFF: true

    # Step 2: Apply ipAllowList against the normalized IP
    local-only:
      ipAllowList:
        sourceRange:
          - "127.0.0.1/32"       # localhost
          - "192.168.1.0/24"     # your LAN
          - "100.64.0.0/10"      # Tailscale CGNAT
          - "YOUR_WAN_IP/32"     # your public IP
        ipStrategy:
          depth: 0               # trust the plugin’s XFF

  routers:
    whoami:
      entryPoints:
        - "websecure"
      rule: "Host(`whoami.example.com`)"
      middlewares:
        - realip
        - local-only
      service: whoami
      tls: {}

Key points added here:

  • In bridge mode, traefik sees only the docker gateway IP by default.
  • The traefik-get-real-ip plugin overwrites X-Forwarded-For with the true client IP, regardless of whether traffic comes via Cloudflare, Pangolin, or directly.
  • Setting ipStrategy.depth: 0 ensures ipAllowList uses the sanitized IP, not the gateway.

This way, you can keep traefik isolated in bridge mode without losing client IP visibility.

I hope this makes guide more clear. See you around :slight_smile:

1 Like