Securing the Traefik Dashboard/API with Cloudflare Access
I recently setup a new cloud VPS for this blog, couple of apps, and a sites for my business. I decided to use Traefik Proxy to help orchestrate application deployments better than manually configuring Apache httpd. Traefik also integrates nicely with Docker, making it super simple to get everything working the wait I want. There is a built-in dashboard that comes with Traefik that gives you a quick overview of what's going on, although it isn't secured by default. There are a number of options to enable secure access with middleware, like HTTP Basic authentication or IP address whitelisting. Those methods feel so 2000s to me, and I knew there was a better way. Enter, Cloudflare Access.
Cloudflare Access is a Zero-Trust access control solution for authenticating connections to internal/corporate web applications. It's also way way faster than connecting with a VPN. I can also leverage my Google Workspace domain as an identity provider for Access.
To authenticate requests to the Traefik dashboard, we'll be using a ForwardAuth
middleware. This middleware makes an HTTP request to an external service with headers from the original request. If the authentication service returns an HTTP 2xx
status code, access is allowed. Otherwise, Traefik sends the response from the service back and access is denied.
Cloudflare helpfully includes a Cf-Access-Jwt-Assertion
header with every request, which is what I'll use to validate access to Traefik. This token is generated when you login to Cloudflare Teams/Access with your configured identity provider. Cloudflare provides a way to validate these tokens at the origin and all I need is a simple Python Flask server that consumes the JWT header, fetches the signing keys, and validates the token.
Validating the JWT
Cloudflare provides some sample Python code to validate the JWT, which can be adapted into a simple method that takes the JWT from the HTTP header and the audience tag generated for your Access Application.
import jwt
TEAMS_DOMAIN='test.cloudflareaccess.com'
def validate_jwt(token, aud):
jwks_client = jwt.PyJWKClient('{}/cdn-cgi/access/certs'.format(TEAMS_DOMAIN))
signing_key = jwks_client.get_signing_key_from_jwt(token)
try:
jwt.decode(token, signing_key.key, ["RS256"], options={'require':['exp', 'iat', 'email']}, audience=aud, issuer='https://'+TEAMS_DOMAIN)
return True
except:
return False
Configure Traefik
Setup Access
- Create a proxied A, AAAA, or CNAME record that points to the public IP of the server.
- Add a new Access application in Cloudflare for Teams dashboard.
- Copy the AUD tag.
Docker Container
Here's a sample docker-compose.yml
file that enables the middleware and uses a docker image I created.
services:
traefik:
image: "traefik:2"
ports: ["443:443"]
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.entrypoints=https"
- "traefik.http.routers.api.middlewares=cloudflare-access@file"
traefik-forwardauth-cloudflare-access:
image: ghcr.io/byte-method/traefik-forwardauth-cloudflare-teams:latest
environment:
CF_TEAMS_DOMAIN: "test.cloudflareaccess.com"
Testing
To test that the configuration is working, I'll attempt to connect directly to the public IP of the server:
$ curl -ksvo /dev/null https://traefik.example.com/dashboard/ --connect-to ::192.0.2.1 2>&1 | egrep -i "< location|< http"
< HTTP/2 401
Great! The request is denied (HTTP 401
Unauthorized). I should get a login page if I try to access that domain in a web browser.