At Revenni we’re huge fans of Traefik and have used their software for over 3 years. This time last year we decided to deploy TraefikEE for a couple of clients - it has been a fantastic experience and dealing with Traefik on the business end, a pleasure.
There are a literal ton of posts just like this one, but not a single one summarized a working solution to use ipwhitelisting[sic] with Cloudflare. Long story short, a client has a domain behind Cloudflare and its origins are fronted by Traefik.
Host Networking
This post covers the explicit use of Cloudflare, if you landed here because your containers only see the internal ips you’ll want to check out this post by Sebastián Ramírez on host networking. Coles notes for TraefikEE users:
The ports section of the proxy service in proxies.yml
needs to look like this:
ports:
- target: 80
published: 80
protocol: tcp
mode: host
- target: 443
published: 443
protocol: tcp
mode: host
You require host networking to access the source ips within Traefik with or without Cloudflare.
IPWhitelisting middleware
Some resources are more sensitive than others, and some we want to keep out of the public eye. The IPWhiteList middleware can handle this for us. In your dynamic.yml
you can specify the following:
http:
middlewares:
internal-restricted:
ipwhitelist:
sourcerange:
- "123.123.123.123"
- "124.124.124.0/24"
- "111.111.111.111"
Then assign the label in your stack:
- "traefik.http.routers.webservice-secure.middlewares=internal-restricted@traefikee"
Simple as that, you’ve got yourself an acl for the internal service you don’t want accessible to the world.
Cloudflare
Once Cloudflare is thrown into the mix, the X-FORWARDED-FOR header will always be the address the request was received from, aka the Cloudflare proxy ip. This is a Traefik security control as you can arbitrarily pass X-FORWARDED-FOR on any request and bypass any ipwhitelist controls.
According to Cloudflare there are two behaviours:
- A request without a X-FORWARDED-FOR header will have one created with the CF-Connecting-IP value
- A request with a X-FORWARDED-FOR header will have the Cloudflare proxy address appended to the header
The first behaviour appeared to be false as Traefik was overriding the value with the ip the request was received from. Thankfully, Traefik has a forwardedHeaders function you can assign to entrypoints which instructs Traefik to trust the X-FORWARDED-* headers it receives from the client.
Of course, you wouldn’t want to trust a value received arbitrarily from any source, so forwardedHeaders comes with a trustedIPs argument. Conveniently, Cloudflare also publishes a list of their proxy ips. You can also fetch these systematically via their text versions: ipv4 and ipv6.
TraefikEE
Putting this all together in Traefik, we have a couple of additions to the static, dynamic, and stack configs.
Static
What this looks like from your static.yml
perspective follows. The trustedIPs are a concatenated list of the ipv4+ipv6 files above, you will have to update this from time to time, however, Cloudflare doesn’t update these very often and missing an update means some people will get a Forbidden error. It’s better to fail to a safe state.
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
forwardedHeaders:
trustedIPs:
- 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
- 2400:cb00::/32
- 2606:4700::/32
- 2803:f800::/32
- 2405:b500::/32
- 2405:8100::/32
- 2a06:98c0::/29
- 2c0f:f248::/32
As usual, you can apply your static configuration with teectl apply --file="static.yml"
Now when a request is received from one of the Cloudflare proxies, the manipulation of the X-FORWARDED-* headers will be visible, specifically, the second behaviour Cloudflare notes above is visible to Traefik and we can build ipwhitelist entries based on it. An example:
GET /assets/media/favicons/android-chrome-192x192.png HTTP/1.1
Host: website.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: image/webp,*/*
Accept-Encoding: gzip
Accept-Language: en-US,en;q=0.5
Cdn-Loop: cloudflare
Cf-Connecting-Ip: 123.123.123.123
Cf-Ipcountry: CA
Cf-Ray: 6abb6e93cab732d9-EWR
Cf-Visitor: {"scheme":"https"}
Cookie: connect.sid=s%3ABj-YMIZtrKauO30VgsYY2NqORQC0cLPis
Dnt: 1
X-Forwarded-For: 122.122.122.122, 172.70.114.169 *<--- first ip is the real client IP (122.122.122.122)
X-Forwarded-Host: website.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: fba767e
X-Real-Ip: 172.70.114.169
Dynamic
Next up are the dynamic.yml
changes. There is a new definition here, ipstrategy. Traefik documentation seems incorrect as it states The depth option tells Traefik to use the X-Forwarded-For header and take the IP located at the depth position (starting from the right)
. Their examples are also contradictory to my testing as the genuine ip in X-FORWARDED-FOR header above is 122.122.122.122 and it’s accessed at depth 1. So either the counter starts at 0 (which is ignored by Traefik) or the position is calculated from the left.
I chose to create a second middleware specifically for stacks that use Cloudflare in dynamic.yml
.
middlewares:
internal-restricted-cloudflare:
ipwhitelist:
sourcerange:
- "111.111.111.111"
- "122.122.122.122"
- "133.133.133.133"
- "144.144.144.144"
ipstrategy:
depth: 1
Once added you can apply the dynamic configuration by running teectl apply --file="dynamic.yml"
.
Stack
In your stack, we add the new middleware as a label to your service
- "traefik.http.routers.webservice-secure.middlewares=internal-restricted-cloudflare@traefikee"
Then redeploy the stack.
Hopefully, the hours poured into this save you a bit of time. Let me know in the comments if you encounter any issues.