gero.dev LogoGero Gerke
Published on

cloudflared + traefik + docker for web-hosting without opened ports

Title Image: Network switch in a homelab setting.

Introduction

It's time for another homelab revamp. This time, I am moving most of my infrastructure from the cloud onto a bare metal server in colocation. Due to some peculiarities, every global firewall port rule has to be requested from the network operating center of my network provider. Therefore, I was looking into some options to serve web-content from my server without having to request a firewall rule for every VM. Theoretically, this setup could also be used to service web-content from behind a NAT or another situation where there is no global IP available. Turns out, only one additonal component is necessary: Cloudflare Tunnels. They provide a link between your server and Cloudflare and do not require any opened ports. In this blog post, I'll go over my setup for full encryption between the web-service and my users and some considerations to take when building something similar.

Configuration

traefik + cloudflared

Cloudflare Tunnels run via cloudflared, a software-daemon from Cloudflare that opens an outgoing connection to the closest Cloudflare points-of-presence to proxy your internet traffic. Since Cloudflare Tunnels only provide limited routing functionality (only path based), we use it together with the popular reverse-proxy Traefik, that integrates well with Docker. Traffic will be routed by Cloudflare to Traefik, which then routes to the individual Docker containers which contain the web-services we want to expose.

I am using the following docker-compose.yml for Traefik and cloudflared.

docker-compose.yml
version: "3.9"
services:
  tunnel:
    container_name: cloudflared-tunnel
    image: cloudflare/cloudflared
    restart: unless-stopped
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=<YOUR TUNNEL TOKEN>
    networks:
      - cftunnel-transport

  traefik:
    image: traefik:2.9
    container_name: traefik
    restart: always
    networks:
      - cftunnel-transport
      - cloudflaretunnel
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yaml:/traefik.yaml:ro
      - ./certificates.yaml:/certificates.yaml:ro
      - ./origin-certificates/:/origin-certificates:ro

networks:
  cftunnel-transport:
  cloudflaretunnel:
    external: true

The Compose file defines a container for Traefik and a container for cloudflared. The Docker network cftunnel-transport is used for transport between Traefik and cloudflared. The Docker network cloudflaretunnel is used to expose Docker containers to Traefik.

To secure traffic between Traefik and cloudflared, a Cloudflare Origin Certificate is used. This can be generated in the Cloudflare dashboard and the files should be saved as mydomain.tld.pem and mydomain.tld.key into the origin-certificates folder. We will instruct Traefik to secure all TLS traffic with these certificates.

certificates.yaml
tls:
  stores:
    default:
      defaultCertificate:
        certFile: /origin-certificates/mydomain.tld.pem
        keyFile: /origin-certificates/mydomain.tld.key

  certificates:
    - certFile: /origin-certificates/mydomain.tld.pem
      keyFile: /origin-certificates/mydomain.tld.key
traefik.yaml
log:
  level: DEBUG

entryPoints:
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: certificates.yaml

Note that there is only a websecure entrypoint. We don't need a HTTP entrypoint, since we will configure services in the Cloudflare Tunnel to use HTTPS for connecting to Traefik only.

Adding a service

To add a service, simply expose it as normal to Traefik.

docker-compose.yaml
version: '3'

services:
  vaultwarden:
    image: traefik/whoami
    container_name: whoami
    restart: always
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`whoami.mydomain.tld`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls=true
      - traefik.http.routers.whoami.service=whoami
      - traefik.http.services.whoami.loadbalancer.server.port=80
    networks:
      - cloudflaretunnel

networks:
  cloudflaretunnel:
    external: true

To configure a service in the Cloudflare tunnel, add simply https://traefik as the destination. For Traefik to know which service to route the request to, we also have to specify the origin server name. Since Traefik can also speak HTTP/2, we can enable that as well.

Configuration for a Hostname in the Cloudflare Tunnel

Conclusion

Cloudflare Tunnels offer an easy way to expose a web-service securely without having to open any ports. However, using such a service has the drawback of being fully reliant on Cloudflare. It should also be noted that, while traffic between users and Cloudflare is encrypted, and traffic is encrypted between Cloudflare and your origin server, Cloudflare could still see the request contents.

Photo by Thomas Jensen on Unsplash.