Cloudflare Tunnel for Homelab Edge

Expose lab services to the public internet with zero open ports, zero dynamic DNS, and zero exposed origin IPs.

What It Actually Is

Cloudflare Tunnel (formerly Argo Tunnel) is an outbound-only daemon (cloudflared) that holds a persistent connection to Cloudflare's edge. Inbound traffic for your domain arrives at Cloudflare, gets routed through that existing connection, and lands on your local service. No NAT, no port forwards, no exposed WAN IP.

For a homelab on residential internet or CGNAT, this is the cleanest way to publish services. For anyone uncomfortable poking holes in their firewall, it is the right default.

Tradeoffs Worth Naming

  • Cloudflare sees your traffic in clear. They terminate TLS at the edge. If that is unacceptable, use a self-hosted reverse proxy + dynamic DNS instead.
  • Bandwidth and abuse policy: Cloudflare's free plan technically prohibits non-HTML traffic (large media streaming, file hosting). It often works fine until it doesn't.
  • Single vendor dependency. If Cloudflare has an outage, your services are unreachable. Keep an internal access path (Tailscale, WireGuard) for admin.

Prereqs

  • A domain managed by Cloudflare (nameservers pointed at them).
  • A small always-on host on your lab network — LXC, VM, or even a Pi.
  • A reverse proxy in front of your services (NPM, Caddy, Traefik). The tunnel can target the proxy as a single upstream.

Install cloudflared

Debian/Ubuntu:

curl -L https://pkg.cloudflare.com/cloudflare-main.gpg \
  | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null

echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
  https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
  | sudo tee /etc/apt/sources.list.d/cloudflared.list

sudo apt update && sudo apt install cloudflared

Create a Named Tunnel

Authenticate the daemon, then create a tunnel and route DNS to it. The tunnel name and ID are how Cloudflare addresses your endpoint.

cloudflared tunnel login
cloudflared tunnel create homelab
# Note the UUID it prints — that is the tunnel ID.

cloudflared tunnel route dns homelab lab.example.com
cloudflared tunnel route dns homelab '*.lab.example.com'

The wildcard DNS route lets you put many services behind one tunnel without creating new DNS records for each. You handle hostname routing inside the tunnel config.

Ingress Rules: One Tunnel, Many Services

Create /etc/cloudflared/config.yml:

tunnel: homelab
credentials-file: /etc/cloudflared/<tunnel-uuid>.json

ingress:
  # All public services go through NPM on 192.168.1.120
  - hostname: "*.lab.example.com"
    service: https://192.168.1.120:443
    originRequest:
      originServerName: lab.example.com
      noTLSVerify: true   # NPM is using its own self-signed cert here

  # Direct route to a single service (bypass proxy)
  - hostname: grafana.example.com
    service: http://192.168.1.50:3000

  # Catch-all: required at the end
  - service: http_status:404

Pointing the wildcard at your reverse proxy is the high-leverage pattern. Add new services by configuring them in NPM/Caddy — no tunnel changes needed.

Run as a Service

sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

Confirm with cloudflared tunnel info homelab — you should see one or more active connections to Cloudflare edge POPs.

Pair with NPM

In Nginx Proxy Manager, create proxy hosts as usual but use the tunnel-routed hostname (service.lab.example.com) and forward to the internal upstream (http://192.168.1.50:8080). Get certs from Let's Encrypt via the DNS-01 challenge so NPM has a valid cert — Cloudflare will re-encrypt to its edge regardless, but the in-tunnel TLS is cleaner.

See the SSL/TLS Certificates guide for issuing certs without exposing ports 80/443.

Cloudflare Access for Auth

For anything sensitive — admin UIs, internal dashboards — put Cloudflare Access in front of the hostname. It is a JWT-based auth gate that runs at the edge before the request hits your tunnel:

  • In the Zero Trust dashboard, create an Application for the hostname.
  • Set a policy (e.g., "allow emails ending in @example.com" or "require one-time PIN from a specific list").
  • Optionally enforce the JWT check at your reverse proxy by validating the Cf-Access-Jwt-Assertion header.

If you already run Authelia, you generally pick one or the other — stacking both works but doubles the login flow.

Operational Notes

  • Multiple connectors for resilience: run cloudflared on two hosts with the same tunnel credentials. Both register; Cloudflare load-balances and fails over automatically.
  • Metrics endpoint: cloudflared exposes Prometheus metrics on --metrics 127.0.0.1:2000. Scrape it — see the Prometheus + Grafana guide.
  • Updates: apt upgrade cloudflared regularly. The daemon gets meaningful improvements often.
  • WAF and rate limiting live at the Cloudflare zone level. Free plan gets you basic rate limiting and managed rules — use them.

Common Pitfalls

  • Origin TLS verification failures: If your upstream uses a self-signed cert, set noTLSVerify: true in originRequest. Better: get a real cert via DNS-01.
  • WebSocket disconnects: Cloudflare proxies WS fine, but some long-lived connections need connectTimeout and tcpKeepAlive tuning under originRequest.
  • 404 from the wrong service: Ingress rules are matched top-down. Put specific hostnames before wildcards.
  • DNS not resolving: The tunnel route dns command creates CNAMEs pointing at tunnel-uuid.cfargotunnel.com. Verify in the Cloudflare DNS UI; the record must be proxied (orange cloud).

Validation Checklist

  • cloudflared tunnel info homelab shows active connections
  • curl -I https://service.lab.example.com from anywhere returns expected response
  • No inbound ports forwarded on your router (verify with nmap from outside)
  • Sensitive admin UIs are behind Cloudflare Access or Authelia
  • Service is enabled (systemctl is-enabled cloudflared)
  • An internal access path (Tailscale/WireGuard) exists for tunnel outages

- Crafted by Axiom|Spectre