Skip to content

Cloudflare Tunnels

A Cloudflare Tunnel publishes the applications of a host that the Internet cannot reach: a container on a private network, a server behind NAT. A small connector, cloudflared, runs on the host and opens an outbound connection to Cloudflare. Visitors reach Cloudflare, and Cloudflare sends their requests down that connection.

The connector authenticates with a token. Whoever holds it can run a connector for the tunnel, and so receive its traffic. This page installs the connector so that the token never appears on a command line, in a unit file, or in a process list.

What you need

  • A Cloudflare zone that serves your applications' names, and access to its account.
  • A host enrolled in Muppy, with Traefik installed — see Traefik Servers.
  • A DNS domain in Muppy whose records are proxied: the Cloudflare zone itself, or a Muppy Builtin DNS client whose backend is that zone, with Proxied by Default (via MBD Server) checked.

1. Create the tunnel in Cloudflare

  1. In the Cloudflare dashboard, open Zero Trust ▸ Networks ▸ Tunnels, and click Create a tunnel. Choose Cloudflared.
  2. Name it, and save. Create it in the account of your zone: Cloudflare routes a name to a tunnel of its own account only.
  3. The next page offers install commands. Do not run them: each one puts the token on a command line, and cloudflared service install <token> writes it into the unit, where ps and systemctl show give it to every local account.
  4. Copy the Tunnel ID from the tunnel's overview.

Muppy needs an API token to read the tunnel and send it its routes. In My Profile ▸ API Tokens, create a custom token limited to that account, with two permissions:

  • Account ▸ Cloudflare Tunnel ▸ Edit, to read the tunnel and push its routes;
  • Account ▸ Account Settings ▸ Read, for Muppy to find the account the tunnel belongs to. Without it, Sync from Provider fails with Failed to derive Cloudflare Account ID.

2. Declare the tunnel in Muppy

  1. Create a Vault of type Cloudflare API Token, and paste the API token in its password.
  2. Open Network ▸ DNS ▸ Network Tunnels, and create a tunnel:
    • Tunnel Type: Cloudflare Tunnel;
    • Host: the host where cloudflared will run;
    • Tunnel ID: the ID copied above;
    • Cloudflare API Token: the Vault.
  3. Click Sync from Provider. Muppy reads the account, the state of the tunnel and its connector token.
  4. In the Defaults tab, set the route options every new route receives:

    noTLSVerify: true
    

    Traefik answers on https://localhost:443 with a certificate issued for the public name, not for localhost: the connector must not verify it.

A tunnel in Muppy: its Tunnel ID, its API token, and noTLSVerify in its default route options.

3. Install the connector on the host

Run these commands on the host, as a user with sudo.

Install the package from Cloudflare's repository:

sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL 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 any main' \
  | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install -y cloudflared
cloudflared --version

Write the token in a file only root reads. Copy the token from the tunnel's Connector tab in Muppy, then:

sudo install -d -m 0700 /etc/cloudflared
read -rs TUNNEL_TOKEN        # paste the token, then Enter: nothing is echoed
printf '%s' "$TUNNEL_TOKEN" | sudo install -m 0600 /dev/stdin /etc/cloudflared/tunnel-token
unset TUNNEL_TOKEN

read and printf are built into the shell: the token never becomes the argument of a process, so no other account sees it in the process list.

Create the unit, which reads the token from that file:

sudo tee /etc/systemd/system/cloudflared.service >/dev/null <<'EOF'
[Unit]
Description=Cloudflare Tunnel connector
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run --token-file /etc/cloudflared/tunnel-token
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared

Check that the token stays out of sight, then that the connector runs:

ps -o args= -C cloudflared                 # the file path, never the token
systemctl show -p ExecStart cloudflared    # the same
systemctl status cloudflared --no-pager

The connector logs one Registered tunnel connection line per connection it opens to Cloudflare: sudo journalctl -u cloudflared -n 20. In the Cloudflare dashboard, the tunnel shows Healthy.

4. Serve the host's names through the tunnel

On the host's form in Muppy, open the Tunnels tab and add a line:

  • Tunnel: the tunnel;
  • FQDN Pattern: empty, so every name of the host goes through it, or a pattern such as *.example.com;
  • Service URL Template: https://localhost:443, Traefik on the host itself, where the connector runs.

The host's Tunnels tab: the tunnel, every name of the host, sent to Traefik on localhost.

Then reconfigure the applications, with (re)Configure 'Traefik Applications' on the App Server. For each name the host serves, Muppy:

  • writes a proxied CNAME to the tunnel (<Tunnel ID>.cfargotunnel.com) instead of an A record;
  • adds a route to the tunnel, listed in its Published Application Routes;
  • gives the route of a Custom Domain to the application that carries one;
  • pushes the routes to Cloudflare.

When the token leaked

Rotate it, then give the connector the new one:

  1. In the Cloudflare dashboard, open the tunnel and refresh its token. The old token stops working, and the running connector stops with it.
  2. In Muppy, click Sync from Provider on the tunnel: it reads the new token.
  3. On the host, write the new token in /etc/cloudflared/tunnel-token as in step 3, then sudo systemctl restart cloudflared.