
Cloudflare Tunnel on Ubuntu 26.04 LTS is a small daemon called cloudflared. It opens outbound connections to Cloudflare’s network and lets you publish web apps, SSH and internal services without opening any inbound port. Install it from Cloudflare’s APT repository, authenticate with a tunnel token or a local credentials file, and run it under systemd.
Why Bother With a Tunnel at All?
Anyone who has run a public server for a few months knows the log file. Open /var/log/auth.log on a fresh VPS with port 22 exposed and you will find thousands of failed logins from addresses you have never heard of. Put a web server on port 443 and the scanners show up within minutes, probing for wp-login.php, .env, and whatever vulnerability was published last week.
The traditional answer is a firewall, fail2ban, a reverse proxy and a lot of patience. That works. It also leaves you with a public IP address that attackers can hit directly, even if the site sits behind a CDN, because origin IPs leak through old DNS records, mail headers and certificate transparency logs.
A tunnel flips the model. Instead of the internet connecting to your server, your server connects out to Cloudflare. Nothing listens publicly. The origin IP becomes irrelevant, and you can close ports 80 and 443 at the firewall completely. This is also why the setup suits home labs behind CGNAT, small offices with no static IP, and staging servers that should never be indexed or probed.
Ubuntu 26.04 LTS, codenamed Resolute Raccoon, was released on April 23, 2026, and Ubuntu lists standard support until 2031. That makes it a sensible base for a long-lived tunnel host. This guide walks through the full install, covers both management modes, and ends with the failures that actually happen in production.
What You Need Before Starting
Gather these first. It saves a lot of back and forth later.
- A server running Ubuntu 26.04 LTS with sudo access. A 1 vCPU, 1 GB RAM VPS is enough for the tunnel itself.
- A free Cloudflare account and a domain whose DNS is managed by Cloudflare.
- An application to expose. A local web service on
127.0.0.1:8080is a good test target. - Outbound connectivity from the server. No inbound ports are required.
Confirm the release before touching anything:
lsb_release -a
uname -r
Ubuntu’s version history lists an initial kernel of 7.0 for this release, so expect a 7.x kernel string. Then bring the system current:
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y curl ca-certificates gnupg
Reboot if a new kernel was installed. It is boring advice, but a tunnel host that has been running on a stale kernel for months is how nasty surprises start.
Choose Your Management Mode First
Cloudflare Tunnel comes in two flavors, and picking the wrong one for your team creates friction later.
| Aspect | Remotely managed | Locally managed |
|---|---|---|
| Where config lives | Cloudflare dashboard | config.yml on the server |
| Auth method | Tunnel token | Certificate plus credentials JSON |
| Install command | cloudflared service install <TOKEN> |
cloudflared service install |
| Best for | Quick setups, small teams | Git-versioned infrastructure, automation |
| Ingress changes | Edit in dashboard, applied live | Edit file, restart service |
Cloudflare’s current getting-started flow installs cloudflared and runs sudo cloudflared service install <TUNNEL_TOKEN> for the token-based approach. Locally managed tunnels require a CLI setup first, then a service install that reads your config file.
For a single server with a handful of hostnames, remotely managed is faster. For anything you want in Git, reviewed in pull requests and rebuilt by Ansible, go local. This article covers both, starting with the shared install.
Installing cloudflared From the APT Repository
Skip the random .deb download from GitHub. A repository install means apt upgrade keeps the daemon current, and that matters. Cloudflare only supports recent cloudflared versions, and an unattended tunnel that silently ages will eventually stop connecting.
Step 1: Add the 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
This is the key path shown in Cloudflare’s own documentation. Keys get rotated occasionally, so if apt update complains about a signature later, check the Cloudflare packages page for the current key filename before assuming something is broken.
Step 2: 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
The any in that line is deliberate. Cloudflare’s repository uses a generic distribution name, so the same line works across supported Debian and Ubuntu releases. That is handy on a brand new release like 26.04, where a codename-specific suite might not exist yet.
Step 3: Install and verify
sudo apt-get update
sudo apt-get install -y cloudflared
cloudflared --version
You should see a version string with a recent date. Check where the binary landed:
which cloudflared
apt-cache policy cloudflared
The signed-by option scopes the key to this one repository only. That is far safer than the old apt-key add habit, which trusted a key for every repository on the system. It is also the modern, supported way to add third-party sources.
Method A: Remotely Managed Tunnel (Token-Based)
This is the quickest path from nothing to a working public hostname.
Create the tunnel in the dashboard
In the Cloudflare dashboard, go to Networking, then Tunnels, and create a new tunnel. Give it a name that tells you which machine it belongs to, such as prod-web-01 or homelab-nuc. Six months from now, “my-tunnel-2” will mean nothing to you.
Choose Cloudflared as the connector type. The dashboard then displays an install command that includes a long token. Treat that token like a password. Anyone holding it can run your tunnel from any machine.
Install it as a service
sudo cloudflared service install eyJhIjoi...your-token...
That single command writes a systemd unit, enables it at boot and starts it. Check it:
sudo systemctl status cloudflared
journalctl -u cloudflared -n 50 --no-pager
You are looking for lines indicating registered tunnel connections. Cloudflare normally opens several connections to different edge locations for redundancy. Seeing four is normal.
Publish a route
Back in the dashboard, open your tunnel, find Routes and add a published application route. Fill in a subdomain, your domain, and the service URL, for example http://localhost:8080. Cloudflare creates the DNS record for you, a proxied CNAME pointing at the tunnel.
Test from anywhere:
curl -I https://app.example.com
If the application answers, you are done. No port forwarding, no certificates to issue on the origin, no public IP exposure.
Method B: Locally Managed Tunnel (Config File)
Teams that manage servers as code usually prefer this. The configuration is a plain text file you can diff, review and deploy.
Step 1: Authenticate
cloudflared tunnel login
This prints a URL. Open it in a browser, pick your domain, and authorize. A certificate file lands in ~/.cloudflared/cert.pem. On a headless server, copy the URL to your workstation. It works fine from any browser.
Step 2: Create the tunnel
cloudflared tunnel create prod-web-01
The output includes a UUID and the path to a credentials JSON file such as ~/.cloudflared/<UUID>.json. Back that file up in a secrets manager. If it is lost, you will need to create a new tunnel.
Step 3: Move files to a system location
Running a production service out of a personal home directory is asking for trouble. Put everything under /etc/cloudflared:
sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/<UUID>.json /etc/cloudflared/
sudo chown root:root /etc/cloudflared/<UUID>.json
sudo chmod 600 /etc/cloudflared/<UUID>.json
Step 4: Write the config
# /etc/cloudflared/config.yml
tunnel: <UUID>
credentials-file: /etc/cloudflared/<UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
- hostname: ssh.example.com
service: ssh://localhost:22
- service: http_status:404
Two details matter here. The final catch-all rule is mandatory, because cloudflared refuses to start without one. And order counts: rules are evaluated top to bottom, so put specific hostnames before wildcards.
Validate before you run anything:
cloudflared tunnel ingress validate --config /etc/cloudflared/config.yml
cloudflared tunnel ingress rule https://app.example.com --config /etc/cloudflared/config.yml
The second command shows which rule would match a given URL. It catches ordering mistakes in seconds.
Step 5: Create DNS records
cloudflared tunnel route dns prod-web-01 app.example.com
cloudflared tunnel route dns prod-web-01 ssh.example.com
Step 6: Dry run in the foreground
sudo cloudflared tunnel --config /etc/cloudflared/config.yml run
Watch for errors, then hit Ctrl+C. Running once by hand saves you from debugging a systemd failure when the real problem is a typo in YAML.
Step 7: Install as a service
sudo cloudflared --config /etc/cloudflared/config.yml service install
sudo systemctl enable --now cloudflared
There is a trap here that bites many people. Cloudflare’s documentation notes that when the install runs under sudo, $HOME points to /root, so the service may fail to find a config sitting in /home/<USER>/.cloudflared/config.yml, and the fix is to pass the config path explicitly. Keeping the config in /etc/cloudflared avoids the whole problem.
Understanding the systemd Unit
Knowing what the unit looks like makes debugging much easier. A token-based unit looks roughly like this:
[Unit]
Description=Cloudflare Tunnel
After=network.target
[Service]
TimeoutStartSec=0
Type=notify
ExecStart=/usr/local/bin/cloudflared tunnel --loglevel info --logfile /var/log/cloudflared/cloudflared.log run --token <TOKEN VALUE>
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
To change flags, Cloudflare recommends editing the unit with sudo systemctl edit --full cloudflared.service, then restarting. Note that the token appears in the command line, which means it is visible in ps output to local users. On a shared box, that is worth fixing. Use the TUNNEL_TOKEN environment variable instead, loaded from a root-only file:
sudo systemctl edit cloudflared
[Service]
EnvironmentFile=/etc/cloudflared/token.env
Keep the file at mode 600, owned by root. It is a small change, and it removes a secret from process listings and shell history.
Troubleshooting Common Errors
Most failures fall into a short list. Here is what they look like and how they are usually solved.
“failed to unmarshal tunnel credentials” or missing credentials file
The path in credentials-file is wrong, or permissions block the service. Check:
ls -l /etc/cloudflared/
sudo journalctl -u cloudflared -n 100 --no-pager
Use absolute paths. Relative paths and ~ behave differently under systemd than in your shell.
Service fails because it cannot find config.yml
This is the $HOME=/root problem described earlier. Move the config to /etc/cloudflared/config.yml or pass --config explicitly when installing the service.
“no ingress rules were defined” or the catch-all error
Your ingress block must end with a rule that has no hostname, such as - service: http_status:404. Validate with cloudflared tunnel ingress validate.
Error 1033 or 502 in the browser
Error 1033 generally means Cloudflare cannot reach a running tunnel for that hostname. Confirm the service is active and that DNS points to the right tunnel. A 502 usually means the tunnel is up but the local service is not answering. Test the origin directly:
curl -v http://localhost:8080
If that fails, the tunnel is innocent. Fix the application first.
Connection timeouts and “failed to dial to edge”
Outbound traffic is blocked. Check egress rules on the host and any upstream firewall, then test connectivity:
curl -sI https://www.cloudflare.com | head -n 1
If QUIC is blocked, switch to --protocol http2. Cloudflare’s documentation lists the required outbound ports and destinations, so compare them against your firewall rules.
Clock drift
Unusual, but real on VMs restored from snapshots. Skewed time breaks TLS handshakes. Check with timedatectl and make sure NTP sync is active.
The tunnel works, but websites feel slow
Look at the origin first. Run curl -w "%{time_starttransfer}\n" -o /dev/null -s http://localhost:8080 to measure time to first byte locally. If the origin itself takes two seconds, no tunnel setting will rescue it. If local timing is fast, review cache rules and edge location instead.
Config changes do nothing
After editing a local config, restart the service, as Cloudflare’s docs note:
sudo systemctl restart cloudflared
Remotely managed tunnels apply dashboard changes automatically, so a restart is rarely needed there.