Let's connect!

Text (855) 455-2200

Cloudflare Tunnels on Ubuntu

No. 24

Cloudflare Tunnels on Ubuntu cover image
Ubuntu Linux Cloudflare

Close every inbound port on your Ubuntu server and still serve the world. Here's how to run a Cloudflare Tunnel as a locked-down systemd service.

Start here: Securing Ubuntu server access with Tailscale covers locking down SSH and closing every inbound port. This article is the other half, serving your applications to the internet through a Cloudflare Tunnel.

Once an Ubuntu server has all of its inbound ports closed, you still need a way for visitors to reach the things it hosts. A Cloudflare Tunnel solves that by having your server dial out to Cloudflare, so nothing ever needs to connect in.

We did this on FreeBSD last year, where we had to build cloudflared from a local port. Ubuntu is considerably easier, because Cloudflare publishes an official apt repository. We'll install from it, then run the tunnel as an unprivileged user under systemd with the token kept in a locked-down file.

Why a tunnel?

A tunnel is an outbound-only connection from cloudflared on your server to Cloudflare's network. Visitors talk to Cloudflare, and Cloudflare forwards requests down the tunnel to a service on your server, usually one listening on localhost. That gives you three things:

  1. No inbound ports to open, so your firewall can stay closed.
  2. Your server's IP address is never published in DNS.
  3. Cloudflare's WAF, DDoS protection, and Zero Trust access policies sit in front of everything.

Update, update, update

As always, bring the server fully up to date first.

sudo apt update && sudo apt upgrade

Install cloudflared

Add Cloudflare's package signing key:

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

Add the repository:

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

Install it:

sudo apt update && sudo apt install cloudflared

Verify the installation:

cloudflared --version

Create the tunnel

  1. Open the Cloudflare Zero Trust dashboard.
  2. Go to Networks > Tunnels and click Create a tunnel.
  3. Choose Cloudflared as the connector type and give the tunnel a name, such as the server's hostname.
  4. When you reach the install step, ignore the install commands the dashboard shows. We'll set the service up ourselves. Copy the token, which is a long string starting with eyJ.

IMPORTANT: Leave the tunnel's dashboard page open in your browser. You'll need it again shortly.

Create a service account

The tunnel doesn't need root, so give it its own user with no home directory and no login shell:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin cloudflared

Store the token

Put the token in a file that only the cloudflared user can read. First create the locked-down directory and an empty token file:

sudo mkdir -p /etc/cloudflared
sudo touch /etc/cloudflared/token
sudo chown -R cloudflared:cloudflared /etc/cloudflared
sudo chmod 700 /etc/cloudflared
sudo chmod 600 /etc/cloudflared/token

Then read the token from a hidden prompt, so it never appears in the command text. Paste the token when prompted. Nothing will echo back:

read -rsp 'Tunnel token: ' TUNNEL_TOKEN; echo
printf '%s' "$TUNNEL_TOKEN" | sudo tee /etc/cloudflared/token >/dev/null
unset TUNNEL_TOKEN

Note: Anyone who can read this token can run your tunnel. Treat it like a password, and avoid pasting it into shared terminals or tickets. If it ever leaks, you can roll it from the tunnel's page in the dashboard.

Keeping the token in a file, and entering it at a hidden prompt, rather than passing it on the command line as cloudflared service install <token> does, keeps it out of the process list and your shell history.

Create the systemd service

Create the unit file:

sudo nano /etc/systemd/system/cloudflared.service

The file contents should be:

[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=cloudflared
Group=cloudflared
Environment=TUNNEL_TOKEN_FILE=/etc/cloudflared/token
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run
Restart=on-failure
RestartSec=5s
TimeoutStartSec=0

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target

A few notes on this file:

  • TUNNEL_TOKEN_FILE tells cloudflared to read the token from the file we just created. See Cloudflare's documentation for the token-file parameter.
  • --no-autoupdate is deliberate. apt owns updates on this server, and a self-updating binary would fight with the package manager.
  • Type=notify lets systemd wait until the tunnel has actually connected before it considers the service started.

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

NOTE: If the service fails to start, check the logs:

sudo journalctl -u cloudflared -f

Route traffic to your service

Return to the tunnel's page in the Cloudflare dashboard. Under Connectors you should now see your server with a status of Connected. Click Next to continue.

Add a public hostname to route traffic to your service:

  • Subdomain and domain: the public address you want, such as app.example.com.
  • Service type: HTTP for most web applications.
  • URL: where your service is listening on the server, such as localhost:8080.

Save the tunnel. Cloudflare creates the DNS record for you automatically. Browse to your new hostname to confirm that requests are reaching your application.

TIP: Bind your application to 127.0.0.1 rather than 0.0.0.0. The tunnel connects from the same machine, and a service that only listens on localhost cannot be reached any other way, even if the firewall is misconfigured later.

Check your firewall

If you followed our Tailscale article, your firewall already denies all incoming connections and allows all outgoing ones. That's exactly what a tunnel needs, because cloudflared only makes outbound connections (to Cloudflare on port 7844, over both TCP and UDP).

If your outbound rules are stricter than ours, allow that port or the tunnel won't connect. Otherwise there is nothing to change. The firewall rules themselves are covered in the Tailscale article, so instead check what is actually listening on the server:

sudo ufw show listening

This lists every listening port with the address it's bound to and the program behind it. Your application should appear bound to 127.0.0.1 rather than 0.0.0.0 or [::]. Anything bound to a wildcard address is reachable on whatever interfaces the firewall allows, so fix the application's bind address rather than relying on the firewall alone.

Keeping cloudflared up to date

Because we installed from Cloudflare's apt repository, updates arrive with the rest of your system's updates:

sudo apt update && sudo apt upgrade

Restart the service afterward so the new binary is picked up:

sudo systemctl restart cloudflared

Note: Cloudflare only supports versions of cloudflared from the last year, so don't let this one drift for too long.

Conclusion

Reboot the server and verify that the tunnel comes back on its own:

sudo systemctl status cloudflared

You now have an Ubuntu server with no open inbound ports that still serves your applications to the world through Cloudflare.

Cheers 🥂

About the author

More Articles