Skip to main content

Exposing Services using Cloudflare Tunnels

· 6 min read
Dario Cruz
Maintainer of DarioCruz.dev

Title card for the blog post: Exposing Services using Cloudflare Tunnels

Hello all,

It's been a while since my last post. Work and life have eaten up a good chunk of my time recently, but luckily I now have some breathing room to post some new content.

Recently, I have been working on building out a Docker server for some network services I wanted to implement, such as AdGuard and Unbound for local DNS resolution. This led me to investigate methods of exposing services running in my lab and home servers to the internet securely.

After researching various methods, I settled on implementing Cloudflare Tunnels alongside Traefik for the reverse proxy and Authelia for secure authentication.

Cloudflare Tunnels

Let's start on the Cloudflare side of things. I moved the DNS for my domain over to Cloudflare to enable tunnel usage. After creating a tunnel for my lab, I created entries and routing for each subdomain. I targeted my Traefik container on port 80, which acts as our reverse proxy and sends requests on to Authelia.

Screenshot of the Cloudflare Zero Trust dashboard showing configured tunnels

The Proxy-Stack

I set up a basic server running Debian Linux and installed Docker with the intent of using Docker Compose for the setup and deployment of my services. With Docker installed and running, I created a folder called proxy-stack and proceeded to hash out the Docker Compose file.

Here we have the configuration for cloudflared, which is our connector between Cloudflare's distributed, outbound-only reverse proxy network and my local network.

services:
# Cloudflare tunnels service
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run
environment:
# Tunnel token stored in .env file and passed as a variable.
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
# Network for all containers in the proxy stack.
networks:
- proxy-net

I passed the tunnel token via a .env file. This token is obtained from the Cloudflare dashboard and is specific to the tunnel you created. The command tunnel run initiates the connection to Cloudflare's tunnel servers.

Notice the proxy-net network defined in the YAML file. I defined a specific network so that all components of this reverse proxy can communicate with one another without exposing other containers running on the server.

Traefik provides the local reverse proxy functionality; it makes use of the Docker API for the discovery of containers configured with Traefik labels. I have locked the image to version 3.6 for stability and passed some commands to configure Traefik on startup:

  • --api.insecure=false - Disables public access to the management GUI.
  • --providers.docker=true - Enables listening to the Docker daemon to automatically spin up routes based on labels.
  • --providers.docker.exposedbydefault=false - Disables routing to every container, scoping it only to those with explicit labels.
  • --entrypoints.web.address=:80 - Defines the global entry point for Traefik on port 80 inside the proxy-net network.
  • --entrypoints.web.forwardedHeaders.insecure=true - Enables trusting upstream proxy headers (required when using Cloudflare Tunnels upstream).
  traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.forwardedHeaders.insecure=true"
networks:
- proxy-net
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro

Finally, we have Authelia, our authentication server. I have configured it with labels that define how Traefik should handle connectivity and access:

  • traefik.http.routers.authelia.rule=Host('auth.dariocruz.dev') - Routes traffic from the subdomain to the Authelia service.
  • traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth - Informs Traefik to forward request validations to Authelia's internal API.
  • traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true - Enables trusting of upstream headers.
  • traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email - Populates headers upon successful login for SSO functionality.
  authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia:/config
networks:
- proxy-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host('auth.dariocruz.dev')"
- "traefik.http.routers.authelia.entrypoints=web"
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
- "traefik.docker.network=proxy-net"
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"

How this all works in practice

Now that the core stack is running, here is how I set up a new service. Let's deploy Linuxserver/Obsidian, a containerized version of Obsidian.

First, I create a subdirectory and a new Docker Compose file:

Screenshot of the file explorer showing the obsidian project directory structure

In the Compose file below, we put the Obsidian container on the same proxy-net. We enable Traefik via labels, define the subdomain, and reference the Authelia middleware we created earlier.

services:
obsidian:
image: lscr.io/linuxserver/obsidian:latest
container_name: obsidian
restart: unless-stopped
security_opt:
- seccomp:unconfined # Needed for GUI rendering
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./obsidian/config:/config
networks:
- proxy-net
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy-net"
- "traefik.http.routers.obsidian.rule=Host('notes.dariocruz.dev')"
- "traefik.http.routers.obsidian.entrypoints=web"
- "traefik.http.services.obsidian.loadbalancer.server.port=3000"
# Reference the Authelia middleware defined in the proxy-stack
- "traefik.http.routers.obsidian.middlewares=authelia@docker"

networks:
proxy-net:
external: true

Next, we update the Authelia configuration file. When Traefik passes traffic to Authelia, Authelia checks its access_control rules. We want to set the policy for our Obsidian service to two_factor to keep our notes secure.

Screenshot of the Authelia configuration.yml showing the access_control section

I added the entry for notes.dariocruz.dev at the bottom of the access_control section:

Close-up of the Authelia ACL rule for notes.dariocruz.dev with two_factor policy

After saving, I restarted the container with docker restart authelia. On the Cloudflare side, I navigated to Networks > Tunnels and added a new public hostname targeting http://traefik:80. Since cloudflared is on the proxy-net, it can reach Traefik directly via its container name.

Screenshot of the Cloudflare dashboard showing public hostnames for the tunnel

I went ahead and added a new route as a published application.

Cloudflare dashboard interface for adding a new public hostname to a tunnel

I then configured the subdomain to use and provided the URL for our Traefik reverse proxy.

Screenshot of the Cloudflare subdomain configuration for notes.dariocruz.dev

Now we can spin up our Obsidian container and test authentication and access.

Terminal output showing the successful start of the Obsidian Docker container

Now, navigating to notes.dariocruz.dev successfully triggers the Authelia login portal before granting access to Obsidian.

Browser screenshot of the Obsidian web interface running securely behind Authelia

Note: I had to make minor adjustments to the final Docker Compose to handle websocket connections for the GUI rendering; I will post more on that optimization soon.

Final Thoughts

Exposing services via Cloudflare Tunnels is straightforward. Once your DNS is switched over, it's only a matter of targeting your internal services via hostname or IP. With Traefik and Authelia, you gain a robust layer of security that ensures only authorized users can reach your home lab services.