Cloudflare Tunnel

Chapter 4 — Cloudflare Tunnel

Cloudflare Tunnel is the solution to the single biggest obstacle for home web hosting: the ISP blocking inbound connections. Instead of waiting for inbound traffic that the router will refuse, the tunnel reverses the connection — your server reaches out to Cloudflare, and Cloudflare forwards visitors' requests back through that outbound connection. No open ports. No port forwarding. Works behind Virgin Media, CGNAT, or any other restrictive ISP.

What this chapter covers: How Cloudflare Tunnel works architecturally — why outbound connections bypass ISP restrictions. Installing cloudflared on your server. The two setup methods: dashboard (GUI) and CLI. The config.yml file and ingress rules. Running the tunnel as a permanent systemd service. Adding multiple services through one tunnel. Troubleshooting common failure modes. This chapter assumes DNS is managed by Cloudflare (Chapter 3) — the tunnel cannot create CNAME records in IONOS.

How Cloudflare Tunnel Works

Traditional web hosting requires your router to accept inbound connections on port 80/443 and forward them to your server. ISPs like Virgin Media block these inbound connections — it's why every approach in this session's troubleshooting hit a wall.

Cloudflare Tunnel inverts the model. A small daemon called cloudflared runs on your server and establishes an outbound encrypted connection to Cloudflare's edge network. When a visitor requests osztromok.com, Cloudflare receives it at their edge, then forwards it back through the existing outbound connection to your server. Your server's response travels back the same way.

Traditional hosting (blocked by Virgin Media): Visitor ──────[port 80]──────▶ Router ──▶ Server ❌ Blocked Cloudflare Tunnel (works because it's outbound): Server ──outbound HTTPS──▶ Cloudflare Edge │ Visitor ──HTTPS──▶ Cloudflare Edge │ (routes through existing tunnel) │ ▼ Your Server Key insight: the server initiates the connection TO Cloudflare. ISPs and NAT/firewalls allow outbound connections freely. No port forwarding. No open inbound ports. No router config needed.

The tunnel uses QUIC (or falls back to HTTPS) for the connection between cloudflared and Cloudflare's edge. It maintains multiple persistent connections for redundancy. If cloudflared restarts, it reconnects automatically.

Prerequisites before starting this chapter:
1. DNS for osztromok.com is managed by Cloudflare (Chapter 3 complete)
2. You have a Cloudflare account with osztromok.com added
3. Apache is running on your server (already confirmed — port 80, systemctl status apache2)

Installing cloudflared

# ── Debian / Ubuntu (your server's OS) ────────────────────────── # Add Cloudflare's package repository $ 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 $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list $ sudo apt update && sudo apt install cloudflared -y # Verify installation $ cloudflared --version cloudflared version 2024.x.x (built 2024-xx-xx)

Two Ways to Set Up a Tunnel

Dashboard Method (Recommended for beginners)
Configure everything through Cloudflare's web UI. The dashboard creates the tunnel, generates credentials, and creates DNS records automatically.

Pros:
  • Guided — less chance of mistakes
  • DNS records created automatically
  • Easy to add/edit public hostnames later
  • Tunnel visible and manageable in the UI
Cons:
  • Requires browser access to Cloudflare
  • Config stored in Cloudflare, not on your server
CLI Method (More control)
Create the tunnel via the cloudflared command line, write a local config.yml, run as a systemd service.

Pros:
  • Config lives on your server — easy to back up
  • More control over ingress rules
  • Better for multiple services / complex routing
  • Easier to version control
Cons:
  • More steps — credentials, config file, DNS
  • Need to create DNS records manually (or via CLI)

This chapter covers both methods. The dashboard method gets you running fastest; the CLI method gives you a setup that's easier to maintain long-term. Both end up with the same result.

Method 1 — Dashboard Setup

Setup Walkthrough · Dashboard Method
Create a tunnel through the Cloudflare Zero Trust dashboard and connect osztromok.com to Apache on your server.
1
Navigate to Zero Trust. In the Cloudflare dashboard, click Zero Trust in the left sidebar (or go to one.dash.cloudflare.com). If prompted to set up a team name, enter anything — it doesn't matter for tunnel use.
2
Create the tunnel. Go to Networks → Tunnels → Create a tunnel. Select Cloudflared as the connector type. Give the tunnel a name — something like home-server. Click Save tunnel.
3
Install the connector. Cloudflare will show you an install command. Select Debian as the OS. It will look like:
$ sudo cloudflared service install eyJhIjoiYWJjMTIzLi4u... # This long token contains your tunnel credentials. # Run this on your server — it installs cloudflared as a systemd service # and writes the credentials automatically. No separate config.yml needed. $ sudo systemctl start cloudflared $ sudo systemctl status cloudflared
After running this, the connector dot in the Cloudflare dashboard should turn green.
4
Add a public hostname. In the tunnel configuration, go to the Public Hostname tab and click Add a public hostname:

Subdomain: leave blank (for root domain) or enter www
Domain: select osztromok.com
Type: HTTP
URL: localhost:80

Click Save. Cloudflare automatically creates the DNS CNAME record for you.
5
Test it. Open a browser (or use curl from another machine) and visit http://osztromok.com. If Apache is running and the tunnel is healthy, you should see your site.
$ curl -I http://osztromok.com HTTP/1.1 200 OK Server: Apache/2.4.57 (Debian) CF-Ray: 7f3a... ← CF-Ray header confirms traffic went through Cloudflare
The dashboard method stores the tunnel token inside the systemd service file rather than a separate config.yml. This is fine for a single-service setup. If you later need multiple services (e.g. a separate app on port 8080), switch to the CLI method or add additional public hostnames through the dashboard.

Method 2 — CLI Setup with config.yml

Step 1 — Authenticate cloudflared with your Cloudflare account

$ cloudflared tunnel login Please open the following URL and log in with your Cloudflare account: https://dash.cloudflare.com/argotunnel?callback=... # Open this URL in a browser, log in, and select osztromok.com. # cloudflared saves a certificate to ~/.cloudflared/cert.pem You have successfully logged in. If you wish to copy your credentials to a server, they have been saved to: /home/philip/.cloudflared/cert.pem

Step 2 — Create the tunnel

$ cloudflared tunnel create home-server Tunnel credentials written to /home/philip/.cloudflared/abc12345-... -def6-7890-ghij-klmnopqrstuv.json. Created tunnel home-server with id abc12345-def6-7890-ghij-klmnopqrstuv # Note the tunnel ID — you'll need it in config.yml. # The .json file is the tunnel credential — keep it safe. # Move credentials to /etc/cloudflared/ so the system service can read them $ sudo mkdir -p /etc/cloudflared $ sudo cp ~/.cloudflared/abc12345-*.json /etc/cloudflared/ $ sudo chmod 600 /etc/cloudflared/abc12345-*.json # List tunnels to confirm it was created $ cloudflared tunnel list ID NAME CREATED STATUS abc12345-def6-7890-ghij-klmnopqrstuv home-server 2026-06-14T21:00:00Z inactive

Step 3 — Create the config.yml file

# /etc/cloudflared/config.yml tunnel: abc12345-def6-7890-ghij-klmnopqrstuv # your tunnel ID credentials-file: /etc/cloudflared/abc12345-def6-7890-ghij-klmnopqrstuv.json ingress: # Route the root domain to Apache - hostname: osztromok.com service: http://localhost:80 # Route www to the same place - hostname: www.osztromok.com service: http://localhost:80 # Catch-all rule — REQUIRED, must be last # Any request that doesn't match a hostname above gets a 404 - service: http_status:404
The catch-all rule is mandatory. config.yml must end with an ingress rule that has no hostname — it catches requests that don't match any other rule. Without it, cloudflared will refuse to start. The value http_status:404 returns a 404 for unmatched requests, which is the correct behaviour.

Step 4 — Create DNS records for the tunnel

# Tell Cloudflare to create a CNAME DNS record pointing the hostname # to this tunnel. Run once for each hostname in your ingress rules. $ cloudflared tunnel route dns home-server osztromok.com 2026/06/14 21:05:00 Added CNAME osztromok.com which will route to this tunnel tunnelID=abc12345... $ cloudflared tunnel route dns home-server www.osztromok.com 2026/06/14 21:05:01 Added CNAME www.osztromok.com which will route to this tunnel tunnelID=abc12345... # Verify the records were created in Cloudflare DNS $ dig osztromok.com +short abc12345-def6-7890-ghij-klmnopqrstuv.cfargotunnel.com. # CNAME (Cloudflare flattens this)

Step 5 — Test the tunnel before installing as a service

# Run in the foreground first — you'll see connection logs and can confirm it works $ cloudflared tunnel --config /etc/cloudflared/config.yml run 2026/06/14 21:06:00 Starting metrics server on 127.0.0.1:... 2026/06/14 21:06:00 Registered tunnel connection connIndex=0 ip=198.41.200.13 2026/06/14 21:06:00 Registered tunnel connection connIndex=1 ip=198.41.192.47 2026/06/14 21:06:00 Registered tunnel connection connIndex=2 ip=198.41.200.13 2026/06/14 21:06:00 Registered tunnel connection connIndex=3 ip=198.41.192.47 # 4 connections registered = healthy tunnel. Open a browser and test now. # Press Ctrl+C to stop once confirmed working.

Step 6 — Install as a systemd service

# Install cloudflared as a system service (reads /etc/cloudflared/config.yml) $ sudo cloudflared --config /etc/cloudflared/config.yml service install 2026/06/14 21:10:00 Installing cloudflared client as a systemd service 2026/06/14 21:10:00 cloudflared client was successfully installed as a systemd service # Enable and start the service $ sudo systemctl enable cloudflared $ sudo systemctl start cloudflared $ sudo systemctl status cloudflared ● cloudflared.service - cloudflared Loaded: loaded (/etc/systemd/system/cloudflared.service) Active: active (running) since Sun 2026-06-14 21:10:05 BST; 5s ago # "active (running)" = tunnel is up and will restart automatically on reboot

Routing Multiple Services Through One Tunnel

A single tunnel can serve multiple domains and subdomains to different backend services. You don't need a separate tunnel for each service — just add more ingress rules to config.yml and create the corresponding DNS records.

Example: Multiple services on one tunnel
osztromok.com
http://localhost:80
Main Apache site
www.osztromok.com
http://localhost:80
Same as root
blog.osztromok.com
http://localhost:2368
Ghost blog on port 2368
grafana.osztromok.com
http://localhost:3000
Grafana dashboard
(catch-all)
http_status:404
Required — must be last
# /etc/cloudflared/config.yml — multiple services example tunnel: abc12345-def6-7890-ghij-klmnopqrstuv credentials-file: /etc/cloudflared/abc12345-def6-7890-ghij-klmnopqrstuv.json ingress: - hostname: osztromok.com service: http://localhost:80 - hostname: www.osztromok.com service: http://localhost:80 - hostname: blog.osztromok.com service: http://localhost:2368 - hostname: grafana.osztromok.com service: http://localhost:3000 - service: http_status:404
# After editing config.yml, reload the service $ sudo systemctl restart cloudflared # Create DNS records for any new hostnames you added $ cloudflared tunnel route dns home-server blog.osztromok.com $ cloudflared tunnel route dns home-server grafana.osztromok.com

Troubleshooting

# Check the service status $ sudo systemctl status cloudflared # Watch live logs $ sudo journalctl -u cloudflared -f # Check the tunnel is registered in Cloudflare $ cloudflared tunnel info home-server NAME: home-server ID: abc12345-... CONNECTIONS: 4 active ← healthy: 4 connections to Cloudflare edge # Validate config.yml syntax before restarting $ cloudflared tunnel --config /etc/cloudflared/config.yml ingress validate Validating rules from /etc/cloudflared/config.yml OK
Service starts but website shows "ERR_CONNECTION_REFUSED" or "522" error
Apache isn't running or isn't listening on port 80. Check: systemctl status apache2 and ss -tlnp | grep :80. The tunnel forwards traffic to localhost:80 — if nothing is there to receive it, you get a connection error.
Tunnel is "active" in systemctl but website doesn't load — just times out
DNS record not pointing to the tunnel. Check: dig osztromok.com +short — should return a Cloudflare IP or the tunnel CNAME. If it still returns your home IP (82.2.236.221), the CNAME record wasn't created. Run cloudflared tunnel route dns home-server osztromok.com and check the Cloudflare DNS dashboard.
Cloudflare shows "526 Invalid SSL certificate" or "525 SSL handshake failed"
SSL/TLS mode mismatch. In Cloudflare dashboard → SSL/TLS → Overview, check the mode. Flexible = Cloudflare speaks HTTPS to visitors, HTTP to your server (no server cert needed). Full = requires HTTPS on your server. Full (Strict) = requires a valid signed certificate on your server. If you haven't set up Let's Encrypt yet, use Flexible for now.
cloudflared service won't start — "no such file or directory" for credentials
The credentials JSON file path in config.yml doesn't match the actual file. Check: ls /etc/cloudflared/ and verify the filename matches what's in the credentials-file: line exactly, including the full UUID.
"config.yml: ingress: rule #X has no service" — tunnel refuses to start
The catch-all rule at the end of ingress is missing or malformed. It must be the last rule and must have no hostname: key — just - service: http_status:404. Check indentation too — YAML is whitespace-sensitive; use spaces, never tabs.
Tunnel was working, stopped after router reset / reboot
The service may not be enabled to start on boot. Check: systemctl is-enabled cloudflared. If it says disabled, run sudo systemctl enable cloudflared. The tunnel itself doesn't depend on port forwarding — it should survive router changes as long as outbound internet access is available.

Quick Reference — Chapter 4

CommandPurpose
cloudflared tunnel loginAuthenticate with your Cloudflare account — required for CLI method
cloudflared tunnel create NAMECreate a new named tunnel and generate credentials JSON
cloudflared tunnel listList all tunnels on your account and their status
cloudflared tunnel route dns NAME hostCreate a CNAME DNS record in Cloudflare pointing a hostname to this tunnel
cloudflared tunnel --config /etc/cloudflared/config.yml runRun the tunnel in the foreground for testing — Ctrl+C to stop
cloudflared tunnel ingress validateCheck config.yml for syntax errors before restarting the service
cloudflared tunnel info NAMEShow tunnel details and number of active connections to Cloudflare edge
sudo cloudflared service installInstall cloudflared as a systemd service (reads /etc/cloudflared/config.yml)
sudo systemctl enable cloudflaredEnsure tunnel starts automatically on every boot
sudo journalctl -u cloudflared -fWatch cloudflared logs live — first place to look when troubleshooting
File / LocationPurpose
/etc/cloudflared/config.ymlMain tunnel config — tunnel ID, credentials path, ingress rules
/etc/cloudflared/<uuid>.jsonTunnel credentials — treat like a password, don't share or commit to git
~/.cloudflared/cert.pemAccount-level certificate from cloudflared tunnel login