
Exposing a web application to the internet used to mean one thing: punching a hole in your firewall and praying nothing scans port 443 before you finish hardening the box. That approach still works, technically, but anyone who has spent a few years babysitting production servers knows how much attack surface it creates. Cloudflare Tunnel flips that model entirely — instead of opening inbound ports, your server reaches out to Cloudflare’s edge and establishes an encrypted, outbound-only connection. No open ports, no NAT rules, no dynamic DNS scripts running on a cron job at 2 AM.
This guide walks through installing and configuring Cloudflare Tunnel — specifically the cloudflared daemon — on Ubuntu 26.04 LTS, the latest long-term support release. Whether you’re running a WordPress stack behind Nginx, a private SSH bastion, or an internal dashboard that has no business being publicly routable, the underlying installation process is the same. What changes is how you configure ingress rules and how aggressively you lock things down afterward.
A quick reality check before diving in: Cloudflare Tunnel is not a silver bullet. It shifts trust from your perimeter firewall to Cloudflare’s network, which is a reasonable trade for most teams, but it’s still a dependency you need to monitor. If cloudflared crashes and no one notices, your app effectively goes dark even though the origin server is perfectly healthy. That’s a failure mode traditional reverse proxies don’t have, and it’s worth keeping in mind as you build out your systemd services and alerting.
By the end of this article, you’ll have a working tunnel running as a persistent systemd service, DNS routed correctly through Cloudflare, and a checklist of security and performance tweaks that actually matter in production — not just theoretical best practices copy-pasted from documentation.
What Is Cloudflare Tunnel and Why It Matters for Ubuntu Servers
Cloudflare Tunnel (the product) is powered by cloudflared (the binary), a lightweight daemon that creates a persistent, encrypted connection between your origin server and Cloudflare’s global edge network. Traffic flows through Cloudflare first, then gets proxied to your local service over the tunnel — your server never needs a public IP listening on any port.
For sysadmins managing WordPress installs, internal tools, or staging environments, this solves a genuinely annoying problem: exposing services without exposing your server’s actual network footprint. All tunnel connections use TLS 1.3 with post-quantum encryption by default, which is a meaningful upgrade over rolling your own stunnel or relying solely on Let’s Encrypt at the origin.
How It Differs From a Traditional Reverse Proxy Setup
A standard Nginx reverse proxy still requires an open inbound port (usually 443) and a public-facing IP that’s discoverable via DNS. Cloudflare Tunnel removes that requirement entirely — DNS points to a Cloudflare-managed CNAME, not your server’s IP, so port scanners hit a dead end. You can still run Nginx locally for TLS termination between cloudflared and your app if you want defense in depth, but it’s no longer strictly necessary for the internet-facing side.
Prerequisites Before You Start
Get these sorted before touching a terminal — skipping prep work is where most tunnel setups go sideways.
- A Cloudflare account with a domain already added and its nameservers pointed to Cloudflare
- An Ubuntu 26.04 LTS server (VM, bare metal, or cloud instance) with outbound internet access on port 443
- Root or sudo privileges on the target machine
- A local service already running that you want to expose (Nginx, Apache, a Node app, whatever)
- Basic familiarity with systemd, since we’ll be running
cloudflaredas a managed service
One thing that trips people up: Cloudflare Tunnel needs outbound HTTPS access, not inbound. If your server sits behind a corporate proxy or a restrictive egress firewall, you’ll need to allowlist Cloudflare’s edge IP ranges or configure cloudflared to use your proxy explicitly.
Step-by-Step: Installing Cloudflared on Ubuntu 26.04 LTS
Step 1: Update the System
Always start clean. Skipping this step on a fresh Ubuntu 26.04 install is a common source of weird dependency conflicts later.
sudo apt update && sudo apt upgrade -y
Step 2: Add Cloudflare’s Package Signing Key
Cloudflare maintains its own APT repository, which is the cleanest way to install cloudflared and keep it updated through your normal patch cycle rather than manually downloading binaries every time.
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
Step 3: Add the Cloudflare APT 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
Ubuntu 26.04’s codename doesn’t need to match anything in Cloudflare’s repo — the any distribution tag in the repo definition handles that cleanly, which saves you from repo-mismatch errors that plagued earlier Ubuntu releases when Cloudflare hadn’t yet tagged a new codename.
Step 4: Install Cloudflared
sudo apt update
sudo apt install cloudflared -y
Confirm the install:
cloudflared --version
If you see a version string with a recent build date, you’re good. If apt throws a 404 on the repo URL, double-check the keyring path — that’s the single most common install failure people report.
Step 5: Authenticate Cloudflared With Your Cloudflare Account
cloudflared tunnel login
This opens a browser-based auth flow (or gives you a URL to paste into a browser if you’re on a headless server). After you select the hostname/domain you want to associate, it drops a cert.pem file into ~/.cloudflared/. That certificate is what lets your server create tunnels under your account — treat it like an SSH private key, not a throwaway file.
Step 6: Create a Named Tunnel
cloudflared tunnel create production-web
This generates a UUID for the tunnel and a credentials JSON file, also stored in ~/.cloudflared/. Name your tunnels meaningfully — production-web, staging-api, internal-dashboard — because six months from now when you’re troubleshooting at 11 PM, “tunnel-3” tells you nothing.
Verify it exists:
cloudflared tunnel list
Step 7: Create the Configuration File
sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml
A minimal but production-sane config looks like this:
tunnel: <TUNNEL-UUID>
credentials-file: /root/.cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: app.yourdomain.com
service: http://localhost:8080
- hostname: ssh.yourdomain.com
service: ssh://localhost:22
- service: http_status:404
That last line is non-negotiable — Cloudflare requires a catch-all rule at the end of your ingress list, and it should return a 404 for anything that doesn’t match a defined hostname. Forget it, and cloudflared will refuse to start with a config validation error.
Step 8: Route DNS Through the Tunnel
cloudflared tunnel route dns production-web app.yourdomain.com
This creates a CNAME record pointing your subdomain to <UUID>.cfargotunnel.com automatically — no manual DNS editing required, and no risk of a typo in an A record.
Step 9: Install and Run Cloudflared as a Systemd Service
This is the step people skip and regret. Running the tunnel manually in a terminal session means it dies the moment you close the SSH connection.
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
The service install command reads your existing /etc/cloudflared/config.yml, registers cloudflared as a proper systemd unit, and ensures it restarts automatically on boot or crash. Any time you edit the config file afterward, restart the service to load changes:
sudo systemctl restart cloudflared
Real-World Use Cases
Tunnel isn’t just for hobbyists exposing a home lab. In production environments, it solves practical problems:
- WordPress staging environments behind auth-gated tunnels, so client previews don’t need VPN access
- Internal admin dashboards (Grafana, phpMyAdmin, Streamlit apps) exposed only to specific Cloudflare Access policies rather than the open internet
- SSH bastion access for remote teams without maintaining a jump box with a public IP
- API endpoints for CI/CD webhooks that need to be reachable but shouldn’t sit on a server with open inbound ports
For a technical SEO workflow specifically, this is genuinely useful for exposing a headless crawler dashboard or a Streamlit-based rank tracker to a client without provisioning a full VPN setup.
High Availability: Running Multiple Replicas
A single cloudflared process is a single point of failure. For anything customer-facing, Cloudflare recommends deploying multiple replicas — run cloudflared on two or more machines using the same tunnel credentials, and Cloudflare’s edge automatically load-balances and fails over between them. This is the same tunnel UUID, same credentials file, copied to a second server, running as its own systemd service. If one origin goes down during a traffic spike or a kernel update reboot, the other keeps serving without any DNS propagation delay.
Security Hardening for Production Tunnels
Run Cloudflared as a Non-Root User
By default, the service installer often runs as root. In production, create a dedicated system user with minimal privileges:
sudo useradd -r -s /usr/sbin/nologin cloudflared
sudo chown -R cloudflared:cloudflared /etc/cloudflared
Then adjust the systemd unit’s User= directive accordingly and reload:
sudo systemctl daemon-reload
sudo systemctl restart cloudflared
Least-privilege execution is one of Cloudflare’s own recommended hardening steps, and it’s cheap insurance against a compromised binary having full root access.
Lock Down Egress-Only Firewall Rules
Since Tunnel only needs outbound connectivity, configure your firewall (ufw, iptables, or your cloud provider’s security groups) to allow outbound HTTPS to Cloudflare’s IP ranges while denying all unsolicited inbound traffic:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow out 443/tcp
sudo ufw enable
This is the actual security payoff of Tunnel — a fully closed inbound firewall on a server that’s still serving live traffic to the public internet.
Protect the Credentials File and cert.pem
The tunnel credentials JSON and cert.pem are effectively bearer tokens. Anyone with access to those files can spin up a tunnel under your account.
chmod 600 /root/.cloudflared/*.json /root/.cloudflared/cert.pem
Store backups encrypted, never commit them to a repo (yes, this happens more often than you’d think), and rotate them if a server is ever decommissioned.
Layer Cloudflare Access on Top
For anything internal — admin panels, staging environments, monitoring dashboards — pair Tunnel with Cloudflare Access policies requiring authentication (SSO, one-time PIN, or hardware key) before traffic ever reaches your origin. This turns your tunnel into a genuine Zero Trust boundary instead of just a NAT replacement.
Performance Tuning Considerations
Tunnel overhead is usually negligible for typical web traffic, but at scale, a few tweaks matter:
- CPU:
cloudflaredis single-process but can consume noticeable CPU under high concurrent connection counts; monitor withhtopand consider replicas before vertically scaling a single origin - Network latency: since traffic now hops through Cloudflare’s edge, users closest to a Cloudflare PoP see minimal added latency, but geographically distant clients may notice a slight increase compared to a direct connection — usually single-digit milliseconds, rarely worth worrying about
- Connection limits: for high-traffic origins, increase
ha-connectionsin your config to open more simultaneous connections to Cloudflare’s edge, spreading load more evenly - Disk I/O: logging verbosity matters — running
cloudflaredwith--loglevel debugin production generates enormous log volume and unnecessary disk writes; stick toinfoorwarnunless actively debugging
Troubleshooting Common Errors
| Issue | Likely Cause | Fix |
|---|---|---|
| apt update returns 404 for cloudflared repo | Keyring path or repo line mismatch | Re-verify /etc/apt/sources.list.d/cloudflared.list and keyring file permissions |
| Tunnel shows “Down” in dashboard despite service running | Config file not reloaded | Run sudo systemctl restart cloudflared after any config.yml edit |
| Error: no ingress rules were defined | Missing catch-all http_status:404 rule | Add the catch-all as the last ingress entry |
| SSH via tunnel hangs or times out | Wrong service protocol in ingress (using http:// instead of ssh://) | Correct the service scheme to match the actual protocol |
| Systemd service fails immediately after service install | Config file path mismatch or missing credentials file | Confirm credentials-file path in config.yml matches actual file location and permissions |
| DNS not resolving to tunnel | CNAME propagation delay or wrong hostname routed | Run cloudflared tunnel route dns again and check dig output after a few minutes |