Post

Using Cloudflare Tunnel with Traefik

Using Cloudflare Tunnel with Traefik

This tutorial will walk you through how to use Cloudflare Tunnel with Traefik and Google OAuth. You may find individual tutorials elsehwere for how to use each of these applications for your Docker home lab set up. But here we will focus specifically on how to have these services play together nicely with each other. I used the Traefik and Google OAuth tutorials at smarthomebeginner.com to set up Traefik and Google OAuth. I recommend that you follow their super helpful Traefik tutorial and Google OAuth tutorial for the full setup. I will focus here on the changes that you would need for using Cloudflare Tunnel.

To keep this guide self contained, let’s start with a simple example of a Traefik setup from the official guide. The steps here can then be adapted to your actual setup.

Part 1: Basic Traefik setup with Cloudflare Tunnel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
services:
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.2
    # Enables the web UI and tells Traefik to listen to docker
    command: 
      - --api.insecure=true
      - --providers.docker
      - --providers.docker.exposedbydefault=false
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      
  whoami:
    # A container that exposes an API to show its IP address
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.$TODO_YOUR_DOMAIN`)"

I have used the TODO prefix for environment variables which you would need to populate. You can create a .env file in the same directory as your docker compose file to specify the environment variables.

You can test the set up by running

1
curl -H Host:whoami.$TODO_YOUR_DOMAIN http://127.0.0.1

The output should show your IP address and other metadata.

Setting up the Cloudflare tunnel

Follow Cloudflare’s guide to set up a Cloudflare tunnel using the dashboard.

Cloudflare Tunnel configuration page showing mapping from the public domain name to the whoami container present behind the Traefik proxy Adding a public hostname that maps to the new Cloudflare tunnel. Notice that in the URL field we put reverse-proxy, which is the container name of the Traefik container we defined. This works because docker containers within the same network can reach each other using the container names.

1
2
3
4
5
6
7
8
9
  cloudflared:
    image: cloudflare/cloudflared
    container_name: cloudflared
    security_opt:
      - no-new-privileges:true
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=$TODO_CLOUDFLARE_TOKEN
    restart: unless-stopped

You can test the setup using

1
curl -L whoami.$TODO_DOMAIN_NAME

The -L flag allows curl to follow the redirects used by Cloudflare tunnels. You should also be able to use a device outside your local network to visit your website.

Part 2: Using wildcard for subdomains

To minimize configuration overhead, you may wish to avoid having to update the Cloudflare Tunnel configuration every time you want to expose a new Docker container via the tunnel. We can achieve this by using a wildcard DNS entry.

Start by adding another public hostname for your tunnel.

Cloudflare Tunnel configuration page showing adding a new hostname using wildcard* for the subdomain Adding a wildcard public hostname for the Cloudflare tunnel

Notice the warning about DNS records. We need to manually create a wildcard DNS record. This is where our existing CNAME DNS record for whoami will come in handy. We can copy the Target field from the existing record and use it for the new DNS record. This works because we would like the new record to point to the same tunnel. The Target should be of the form TODO_YOUR_TUNNEL_ID.cfargotunnel.com.

Cloudflare DNS configuration page showing creating a new CNAME DNS record Adding a wildcard DNS entry with the same Target as the existing whoami entry.

You can now go ahead and remove the whoami hostname entry from the tunnel configuration page. This will also remove the DNS entry automatically.

You can confirm that the tunnel still works with the new wildcard entry by running

1
curl -L whoami.$TODO_DOMAIN_NAME

Adding a new container behind the proxy will now only require modifying the Treafik configuration.

Part 3: Obtain Google OAuth Credentials

Before we start adding Google OAuth to our Docker setup, we need to set up the OAuth project and obtain the required credentials using the Google Cloud Developer Console. This is explained very well in Step 2 and Step 3 of the Google OAuth guide at smarthomebeginner.com, so I will skip describing the process here.

By the end of this step, you should have your OAuth client set up to redirect to https://oauth.$TODO_YOUR_DOMAIN/_oauth, and have a client ID and client secret generated for you.

Part 4: Adding Google OAuth to Traefik

Let’s start by adding the thomseddon/traefik-forward-auth service to our Docker compose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  oauth:
    container_name: oauth
    image: thomseddon/traefik-forward-auth:2.2-arm
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    environment:
      - CONFIG=/config
      - COOKIE_DOMAIN=$TODO_YOUR_DOMAIN
      - INSECURE_COOKIE=false
      - AUTH_HOST=oauth.$TODO_YOUR_DOMAIN
      - URL_PATH=/_oauth
    secrets:
      - source: oauth_secrets
        target: /config
    labels:
      - "traefik.enable=true"
      # HTTP Routers
      - "traefik.http.routers.oauth-rtr.entrypoints=websecure"
      - "traefik.http.routers.oauth-rtr.rule=Host(`oauth.$TODO_YOUR_DOMAIN`)"
      # Middlewares
      - "traefik.http.routers.oauth-rtr.middlewares=chain-oauth@file"
      # HTTP Services
      - "traefik.http.routers.oauth-rtr.service=oauth-svc"
      - "traefik.http.services.oauth-svc.loadbalancer.server.port=4181"

We also need to make some changes to the configuration of our existing containers.

For reverse-proxy we need to define a new entrypoint for HTTPS(port 443) connections and specify tls for the entrypoint. We can also remove the port forwarding for the HTTP port because we would only be accessing our server through the Cloudflare tunnel and containers within the same network can reach each other’s ports. We also need to map the traefik/rules directory which has our middlewares defined.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.2
    # Enables the web UI and tells Traefik to listen to docker
    command: 
      - --api.insecure=true
      - --providers.docker
      - --providers.docker.exposedbydefault=false
      - --providers.file.directory=/rules
      - --entryPoints.websecure.address=:443
      - --entrypoints.websecure.http.tls=true
    ports:
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      - $TODO_DOCKER_DIR/traefik/rules:/rules # Dynamic File Provider directory

For whoami, we need to specify the chain-oauth middleware.

1
2
3
4
5
6
7
  whoami:
    # A container that exposes an API to show its IP address
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.$TODO_YOUR_DOMAIN`)"
      - "traefik.http.routers.whoami.middlewares=chain-oauth@file"

If we try going to http://whoami.$TODO_YOUR_DOMAIN on a browser, we will get a Bad gateway error. If you started your containers using docker compose up you should be able to see an error message in the logs.

1
cloudflared      | 2024-11-26T05:52:22Z ERR  error="Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared: dial tcp 172.19.0.2:80: connect: connection refused" connIndex=1 event=1 ingressRule=0 originService=http://reverse-proxy

The error makes sense because we now have Treafik listening on the HTTPS port. Let’s fix this by first changing the service type to HTTPS in the Clouldflare tunnel configuration page.

Cloudflare Tunnel configuration page showing the mapping from the public domain to the Traefik HTTPS service Changing the service type to HTTPS for the tunnel

If we try again, we still get the Bad gateway error, but this time a different error message on the terminal.

1
cloudflared      | 2024-11-26T03:05:37Z ERR  error="Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared: tls: failed to verify certificate: x509: certificate is valid for 796d8bfd92656ca7ac5f647d1235e5ec.eb6ac2b89b32eb341bfdbbcc2559f63d.traefik.default, not reverse-proxy" connIndex=1 event=1 ingressRule=0 originService=https://reverse-proxy

This time the issue is that Traefik is using its default SSL certificate since we did not set up our own SSL certificate. We can get around this issue by turning on No TLS verify in the Clouldflare tunnel configuration page.

Cloudflare Tunnel configuration page showing the TLS settings for the tunnelTurning on No TLS Verify for the tunnel

With this setting enabled, Cloudflare will no longer verify the validity of the certificate provided by Traefik. The connection between Cloudflare and your server is via the Cloudflare tunnel and is still encrypted. In the next part, we will cover how to set your SSL certificate so that you don’t have to disable TLS verification.

Part 5: Obtaining your Let’s Encrypt SSL certificate

Using a Let’s Encrypt SSL certificate for our Treafik setup will allow Cloudflare to verify the certificate provided by Traefik when Cloudlfare reaches out to our server.

Using TLS verify

First, let’s turn off No TLS Verify from the Cloudflare tunnel configuration page to be able to test our changes as we go along.

Cloudflare Tunnel configuration page showing the TLS settings for the tunnelTurning off No TLS Verify for the tunnel

Updating Traefik configuration to generate the certificate

Create a directory named acme under the traefik directory you previously created. This directory will host the certificate fetched from Let’s Encrypt.

Follow the instructions at smarthomebeginner.com to create a DNS API token and specify it as a Docker secret. You should end up with a cf_dns_api_token file inside your secrets folder. Note that these instructions are meant for domains that use Cloudflare as their nameserver. Traefik supports multiple other DNS providers. You can find the documentation on their website.

We can now update the docker compose file for our reverse-proxy container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.2
    # Enables the web UI and tells Traefik to listen to docker
    command: 
      - --api.insecure=true
      - --providers.docker
      - --providers.docker.exposedbydefault=false
      - --providers.file.directory=/rules
      - --entryPoints.websecure.address=:443
      - --entrypoints.websecure.http.tls=true
      - --entrypoints.websecure.http.tls.certresolver=dns-cloudflare
      - --entrypoints.websecure.http.tls.domains[0].main=$TODO_YOUR_DOMAIN
      - --entrypoints.websecure.http.tls.domains[0].sans=*.$TODO_YOUR_DOMAIN
      - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # LetsEncrypt Staging Server - uncomment when testing
      - --certificatesResolvers.dns-cloudflare.acme.storage=/acme/acme.json
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.delayBeforeCheck=90 # To delay DNS check and reduce LE hitrate
    ports:
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      - $TODO_DOCKER_DIR/traefik/rules:/rules # Dynamic File Provider directory
      - $TODO_DOCKER_DIR/traefik/acme:/acme # Certs File
      - $TODO_DOCKER_DIR/logs/traefik:/logs # Traefik logs
    environment:
      - CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token
      - TZ=$TZ
    secrets:
      - cf_dns_api_token

Setting Origin Server Name

After updating the configuration, start the containers again using docker compose up. Wait for about 2 minutes for the certificate to be generated. You should see an acme.json file inside the acme folder if everything goes well. Now, when we try going to http://whoami.$TODO_YOUR_DOMAIN on a browser, we will get a Bad gateway error. You should be able to see an error message in the docker logs

1
cloudflared      | 2024-12-24T20:40:55Z ERR  error="Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared: tls: failed to verify certificate: x509: certificate is valid for f7d858dbef2110696cfba8b30a775af4.cbec424d71d1c5316d7088e49b9f8980.traefik.default, not reverse-proxy" connIndex=0 event=1 ingressRule=0 originService=https://reverse-proxy

This error occurs because Treafik returns the default certificate locally generated by Treafik if a request does not correspond to the any of the configured routes. To fix this, we need to specify the Origin Server Name as *.$TODO_YOUR_DOMAIN on the configuration page of our Cloudflare tunnel.

Cloudflare Tunnel configuration page showing the Origin Server Name setting for the tunnelSetting the Origin Server Name for the tunnel

Fetching the actual Let’s Encrypt certificate

Attempting to open http://whoami.$TODO_YOUR_DOMAIN now will result in a different error on the terminal.

1
cloudflared      | 2024-12-24T22:08:51Z ERR  error="Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared: tls: failed to verify certificate: x509: certificate signed by unknown authority" connIndex=2 event=1 ingressRule=0 originService=https://reverse-proxy

This is because we are currently using the staging certificate from Let’s Encrypt. Delete the generated acme.json file and comment out the following line from your docker configuration.

1
# - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory 

Start your containers again and wait for 2 minutes. Traefik should have now fetched the real certificates for your domain. You should now be able to open http://whoami.$TODO_YOUR_DOMAIN and autheniticate using Google OAuth successfully.

This post is licensed under CC BY 4.0 by the author.