2 Cloudflare Tunnel
Drew edited this page 2026-05-22 18:55:45 -04:00

Cloudflare Tunnel

Two independent tunnels run on the host. Both initiate outbound TLS to Cloudflare — no router port-forwarding, no public IP exposed.

Tunnel 1 — cloudflared.service (system, file-configured)

Runs as a systemd unit. Ingress is in /etc/cloudflared/config.yml, credentials in /root/.cloudflared/<TUNNEL_UUID>.json.

tunnel: <TUNNEL_UUID>
credentials-file: /root/.cloudflared/<TUNNEL_UUID>.json
ingress:
  - hostname: lab.justaguylinux.com
    service: http://localhost:80
  - hostname: justaguylinux.com
    service: http://localhost:8080
  - hostname: www.justaguylinux.com
    service: http://localhost:8080
  - hostname: butterbian.org
    service: http://localhost:8082
  - hostname: www.butterbian.org
    service: http://localhost:8082
  - hostname: files.grifmail.cloud
    service: http://localhost:8083
  - service: http_status:404

Reload after edits:

sudo systemctl restart cloudflared
sudo systemctl status cloudflared
sudo journalctl -u cloudflared -f          # tail logs

DNS for a new hostname:

sudo cloudflared tunnel route dns <TUNNEL_UUID> myservice.justaguylinux.com

The catch-all http_status:404 is required as the last rule — Cloudflare rejects configs without it.

Tunnel 2 — cloudflared-thelinuxcast (Docker, token-auth)

Defined in /opt/forgejo/docker-compose.yml. No on-disk ingress file — ingress is configured in the Cloudflare dashboard for the tunnel. Token is the entire config:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared-thelinuxcast
    command: tunnel --no-autoupdate run --token <REDACTED>
    restart: unless-stopped
    network_mode: host

network_mode: host lets it reach localhost:3000 (Forgejo) and any other host port without compose networking gymnastics.

Hostnames it serves (set in the Cloudflare dashboard, not on disk):

Hostname Local port Service
git.thelinuxcast.org :3000 Forgejo web

To inspect or change: dashboard → Zero Trust → Networks → Tunnels → cloudflared-thelinuxcast → Public Hostnames.

Adding a new hostname (Tunnel 1)

  1. Pick a local port (e.g. :9000).
  2. Append a stanza before the http_status:404 catch-all in /etc/cloudflared/config.yml.
  3. Create the DNS record (see next section).
  4. sudo systemctl restart cloudflared
  5. Verify: curl -sI https://myservice.justaguylinux.com should return your origin's status, not Cloudflare's 530 / 1033.

DNS setup

Cloudflare Tunnel works by mapping a hostname → a CNAME record pointing at <TUNNEL_UUID>.cfargotunnel.com, with Cloudflare's proxy turned on (orange cloud). The proxy is what makes Cloudflare receive the request and route it down the tunnel — without proxying, the CNAME would just resolve to a Cloudflare-internal hostname that doesn't answer publicly.

cloudflared writes the DNS record for you:

sudo cloudflared tunnel route dns <TUNNEL_UUID> myservice.justaguylinux.com

What this does:

  • Creates a CNAME myservice.justaguylinux.com → <TUNNEL_UUID>.cfargotunnel.com
  • Sets it to Proxied (orange cloud) automatically
  • Fails if a record with the same name already exists (use --overwrite-dns to replace it)

Use the tunnel's UUID, not its name. Find it with:

sudo cloudflared tunnel list

This requires cloudflared to be authenticated to the Cloudflare account that owns the zone (justaguylinux.com). For the system tunnel (Tunnel 1), that auth is already in /root/.cloudflared/cert.pem. For Tunnel 2 (cloudflared-thelinuxcast), the token-auth tunnel doesn't have a cert.pem — DNS for that tunnel must be created via the dashboard (Method 2).

Method 2 — manual (Cloudflare dashboard)

For token-auth tunnels (Tunnel 2) or when you want explicit control:

  1. Cloudflare dashboard → select the zone (e.g. thelinuxcast.org).
  2. DNS → Records → Add record:
    • Type: CNAME
    • Name: the subdomain (e.g. git) or @ for the apex
    • Target: <TUNNEL_UUID>.cfargotunnel.com
    • Proxy status: Proxied (orange cloud) — required
    • TTL: Auto
  3. Save.

Or via the Zero Trust dashboard:

  1. Zero Trust → Networks → Tunnels → select tunnel → Public Hostnames → Add a public hostname.
  2. Fill in subdomain + domain + service URL (e.g. http://localhost:3000).
  3. The DNS record is created automatically; ingress is stored in Cloudflare (only relevant for token-auth tunnels — file-configured tunnels still read from /etc/cloudflared/config.yml).

Apex domain vs subdomain

  • Subdomain (e.g. lab.justaguylinux.com) — straightforward CNAME. Works exactly as above.
  • Apex (e.g. justaguylinux.com, butterbian.org) — DNS doesn't normally allow CNAME at the zone root, but Cloudflare implements CNAME flattening: a proxied CNAME at @ is resolved server-side and returned as A/AAAA records. So the same cloudflared tunnel route dns command works for the apex on Cloudflare-hosted zones. No special handling needed.
  • www — needs its own record. Both justaguylinux.com and www.justaguylinux.com are configured here, each with its own CNAME → tunnel. The ingress block in config.yml lists both hostnames so traffic to either is served identically.

Verifying DNS

dig +short myservice.justaguylinux.com           # returns Cloudflare anycast IPs (104.21.x / 172.67.x)
dig +short CNAME myservice.justaguylinux.com     # may be empty — CF flattens proxied records at the edge

curl -sI https://myservice.justaguylinux.com     # 200/302/etc from your origin = working
                                                 # 530/1033 = tunnel down or hostname not in ingress
                                                 # 522/523 = origin unreachable from cloudflared

If dig returns Cloudflare IPs but curl returns 1033, the DNS is fine — the issue is in config.yml (hostname missing or typo) or cloudflared isn't running.

Removing a hostname

  1. Remove the stanza from /etc/cloudflared/config.yml.
  2. sudo systemctl restart cloudflared.
  3. Delete the DNS record in the dashboard (or cloudflared tunnel route dns --overwrite-dns is one-way; deletion is dashboard-only).

One tunnel, many zones

A single tunnel can serve hostnames across multiple zones in the same Cloudflare account — Tunnel 1 serves justaguylinux.com, butterbian.org, and grifmail.cloud simultaneously. Each zone needs its own DNS record pointing at the same <TUNNEL_UUID>.cfargotunnel.com. The ingress block routes by hostname.

Why two tunnels?

Tunnel 1 belongs to my Cloudflare account, Tunnel 2 belongs to thelinuxcast's. Two separate tunnels means each of us manages our own DNS in our own dashboard without sharing logins.

Rotating credentials

Tunnel 1 credentials JSON is at /root/.cloudflared/<TUNNEL_UUID>.json. To rotate:

sudo cloudflared tunnel rotate <TUNNEL_UUID>     # regenerates the credentials file
sudo systemctl restart cloudflared

Tunnel 2 token is rotated from the Cloudflare dashboard; the new token must be pasted into /opt/forgejo/docker-compose.yml and the container recreated:

cd /opt/forgejo && docker compose up -d cloudflared