Cloudflare Tunnels on Ubuntu
No. 24
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:
- No inbound ports to open, so your firewall can stay closed.
- Your server's IP address is never published in DNS.
- 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
- Open the Cloudflare Zero Trust dashboard.
- Go to Networks > Tunnels and click Create a tunnel.
- Choose Cloudflared as the connector type and give the tunnel a name, such as the server's hostname.
- 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_FILEtellscloudflaredto read the token from the file we just created. See Cloudflare's documentation for the token-file parameter.--no-autoupdateis deliberate. apt owns updates on this server, and a self-updating binary would fight with the package manager.Type=notifylets 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:
HTTPfor 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.1rather than0.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
cloudflaredfrom 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 🥂