Part 11 walked through Cloudflare's Zero Trust Access policies, showing how to put identity-aware gates in front of internal applications without touching a single firewall rule. That foundation matters here, because this part builds the actual pipe those policies protect. You can configure Access all day long, but if your origin server is still reachable by its raw IP, you haven't closed the loop. Cloudflare Tunnel closes it.
This is where the architecture gets real. Not conceptually real. Practically real, with commands you run, files that get created, and a working HTTPS endpoint pointing at something running on your local machine before the article ends.
Twelve parts in, you've earned the right to stop reading about what Cloudflare can do and start watching it actually do it.
What Is a Cloudflare Tunnel and Why Does It Matter?
The Problem with Traditional Port Forwarding
Most people's first attempt at exposing a home server or internal application to the internet looks the same. Log into the router, find the port forwarding section, point port 443 or 80 at the machine's local IP, and hope the ISP doesn't block it. Sometimes it works. Often it doesn't. And when it does work, the public IP address of that connection is sitting in DNS, visible to anyone who queries it, reachable by anything that can send a packet.
That's not a configuration problem. That's the fundamental shape of the model. Inbound connections require an open door, and open doors invite uninvited guests.
Attackers don't need to break your application to cause damage. They can hammer the IP directly, probe every open port, and map your infrastructure before you've noticed a single unusual log line. Traditional reverse proxies help with application-layer protection, but they don't hide the origin. The IP is still there.
How Cloudflare Tunnel Flips the Model
Cloudflare Tunnel (originally released as Argo Tunnel) inverts the connection direction entirely. Instead of your server waiting for inbound traffic, a small daemon called cloudflared runs on your machine and dials outbound to Cloudflare's edge network. It maintains persistent, encrypted connections to multiple Cloudflare points of presence simultaneously. When a request arrives at your domain, Cloudflare routes it inward through those existing connections to cloudflared, which hands it off to whatever local service you've configured.
Your origin IP never appears in DNS. No port forwarding. No inbound firewall rules. No exposed attack surface at the network layer.
The contrast with a traditional reverse proxy setup is sharp. Nginx or Caddy sitting in front of an application still binds to a public IP. Cloudflare Tunnel doesn't. By the end of this article, you'll have a working tunnel routing real HTTPS traffic to a local service, with your origin completely off the public internet.
Prerequisites and Architecture Overview
What You Need Before You Start
Three things need to be in place before the first command runs. First, a Cloudflare account with at least one domain added to it. Second, that domain's nameservers must be pointed at Cloudflare. Not just partially configured. Cloudflare must be the authoritative DNS provider for the zone, because the tunnel routing commands create DNS records programmatically and that only works when Cloudflare controls the zone. Third, a machine running a local service you want to expose. That service can be anything: a Node app on port 3000, a Grafana dashboard on port 3001, a static file server, a self-hosted tool.
DNS Managed by Cloudflare
If your domain's nameservers are still pointing at your registrar's default DNS, Cloudflare Tunnel routing commands will fail. Confirm the zone shows "Active" status in the Cloudflare dashboard before continuing.
Understanding the Traffic Flow Diagram
The request path looks like this: a browser sends an HTTPS request to app.example.com. That hostname resolves to a CNAME pointing at Cloudflare's tunnel infrastructure. Cloudflare's edge receives the request, applies any Access or WAF policies attached to the zone, then routes the request inward through the persistent outbound connection that cloudflared established. The daemon receives it and forwards it to localhost:3000 on your machine. The response travels back the same path.
The second example scenario follows the same model. dashboard.example.com routes to an internal server on your network, say 192.168.1.50:3001, rather than localhost. The cloudflared daemon handles both. One tunnel, multiple hostnames, multiple backend targets, all configured in a single YAML file. This guide uses the modern Zero Trust dashboard approach alongside CLI commands, rather than the legacy cloudflared tunnel create flow that older tutorials still describe.
Step 1. Installing cloudflared on Your Machine
Installation on Linux (Debian/Ubuntu and RHEL/CentOS)
On Debian and Ubuntu, the cleanest path is the official Cloudflare package repository. Add the GPG key and repo, then install through apt:
1curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
2
3echo "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
4
5sudo apt update && sudo apt install cloudflaredFor RHEL, CentOS, and Fedora systems, use the RPM repository instead:
1curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /etc/pki/rpm-gpg/cloudflare-main.gpg > /dev/null
2
3sudo tee /etc/yum.repos.d/cloudflared.repo <<EOF
4[cloudflared]
5name=Cloudflare
6baseurl=https://pkg.cloudflare.com/cloudflared/rpm/
7enabled=1
8gpgcheck=1
9gpgkey=file:///etc/pki/rpm-gpg/cloudflare-main.gpg
10EOF
11
12sudo yum install cloudflaredIf you're running on a Raspberry Pi or any arm64 machine, the package repository handles architecture detection automatically. Apple Silicon Macs running Linux in a VM will also pull the correct arm64 binary. Always verify after install:
cloudflared --versionCheck the Cloudflare GitHub releases page if you need a specific version or want to confirm you're on the latest build.
Installation on macOS with Homebrew
One line:
brew install cloudflaredHomebrew handles the arm64 vs amd64 distinction automatically on Apple Silicon. After the install completes, run cloudflared --version to confirm the binary is on your path. If Homebrew isn't installed on your machine, the Homebrew install page has the one-liner for that too.
Apple Silicon Users
Homebrew on Apple Silicon installs to /opt/homebrew/bin/ rather than /usr/local/bin/. If cloudflared isn't found after install, confirm that /opt/homebrew/bin is in your shell's PATH.
Installation on Windows
Download the MSI installer from the cloudflared releases page. Run the installer with default settings. After installation, open a new PowerShell or Command Prompt window (not one that was open before the install) and verify:
cloudflared --versionThe installer adds cloudflared to the system PATH automatically. If the command isn't found, a machine restart resolves the environment variable issue in most cases. Windows users on ARM64 hardware should grab the -arm64.msi variant from the releases page rather than the standard installer.
Step 2. Authenticating cloudflared with Your Cloudflare Account
Running the Login Command
Authentication is a single command:
cloudflared tunnel loginRunning this opens a browser window pointed at Cloudflare's authorization page. You'll log into your account and select which zone (domain) you want this installation of cloudflared to be authorized for. After selecting the zone and clicking authorize, Cloudflare writes a credential file to your machine and the terminal confirms completion.
If the browser doesn't open automatically, the terminal prints a URL. Copy it, paste it into any browser on any machine logged into your Cloudflare account, and complete the authorization there. The file gets written to the machine that ran the command regardless of which browser you used.
"The login command doesn't create a tunnel. It creates the authorization that lets your machine create and manage tunnels. That distinction matters when you're thinking about what to protect."
Understanding the cert.pem Credential File
The file cloudflared writes is stored at ~/.cloudflared/cert.pem on Linux and macOS. On Windows it lands in %USERPROFILE%\.cloudflared\cert.pem. This file is an account-level credential. It authorizes cloudflared to create tunnels, manage DNS records, and interact with your Cloudflare account on your behalf.
Protect cert.pem Like a Password
The cert.pem file grants broad access to your Cloudflare account for the authorized zone. Don't commit it to version control. Don't copy it to shared machines. Don't include it in Docker images. Treat it with the same care you'd give an API token with zone-edit permissions.
The cert.pem file is different from the per-tunnel credentials file that gets created in Step 3. The tunnel credentials file is scoped to a single tunnel and is safer to deploy on servers that need to run that specific tunnel. The cert.pem file stays on the machine you use to manage your Cloudflare configuration.
Step 3. Creating Your First Tunnel
Naming Your Tunnel and Understanding Tunnel UUIDs
With authentication complete, create the tunnel:
cloudflared tunnel create my-app-tunnelThe output shows two important pieces of information: the tunnel name you chose, and a UUID that Cloudflare generated. That UUID looks something like 6ff42ae2-765d-4d09-ac85-5e6b7d4c9a3f. The name is a human-readable alias. The UUID is the actual identifier Cloudflare uses internally. If you delete and recreate a tunnel with the same name, the UUID changes. If you rename the tunnel, the UUID stays the same. Build your automation and configuration files around the UUID, not the name.
Confirm the tunnel exists:
cloudflared tunnel listThis prints every tunnel associated with your account for the authorized zone, along with their UUIDs, creation dates, and connector status. A freshly created tunnel shows no active connectors yet. That's expected. The connector appears when cloudflared starts running with this tunnel's configuration.
Where Tunnel Credentials Are Stored
Creating a tunnel writes a JSON credentials file to ~/.cloudflared/<UUID>.json. This file contains a secret that authorizes cloudflared to run as that specific tunnel. It's scoped to one tunnel only. Unlike cert.pem, this file can be copied to a server that needs to run the tunnel without granting that server the ability to create new tunnels or modify DNS records.
The practical implication: keep cert.pem on your management machine. Deploy the JSON credentials file to the server that will actually run cloudflared in production. That separation limits the blast radius if a server is ever compromised.
Step 4. Routing DNS to Your Tunnel
Creating a CNAME Record Pointing to Your Tunnel
With the tunnel created, point a hostname at it. The routing command creates a DNS record automatically:
cloudflared tunnel route dns my-app-tunnel app.example.comCloudflare creates a CNAME record in your zone pointing app.example.com to <UUID>.cfargotunnel.com. The orange-cloud proxy icon appears on that record automatically, meaning traffic to that hostname passes through Cloudflare's network rather than resolving directly to an origin IP. That's not optional. The entire model depends on it.
Verify the record appeared by opening the Cloudflare dashboard, navigating to your zone, and clicking DNS. The new CNAME should be visible within seconds. The proxy status must show the orange cloud. If it shows grey, click the icon to toggle it back to proxied.
Example: app.example.com and dashboard.example.com
Route the second hostname the same way:
cloudflared tunnel route dns my-app-tunnel dashboard.example.comBoth app.example.com and dashboard.example.com now point at the same tunnel UUID via CNAME. The tunnel handles both hostnames. Which backend service each hostname reaches is determined by the configuration file you'll write next, not by DNS. DNS just gets traffic to the tunnel. The config file decides what happens after that.
A grey-cloud CNAME on a tunnel record causes requests to resolve the CNAME target directly. The CNAME target is <UUID>.cfargotunnel.com, which isn't a publicly routable address for your service. Requests will fail, and they'll fail in a confusing way because DNS resolves successfully but the connection goes nowhere useful. Always confirm the orange cloud is active on every hostname you route to a tunnel.
Part 13 moves from DNS routing into the configuration file itself, where you'll map each hostname to its backend service, set connection options, and configure the ingress rules that control exactly how cloudflared handles requests once they arrive at your machine.
Step 5. Writing Your Tunnel Configuration File
Part 11 covered authentication and tunnel creation. You have a tunnel ID and a credentials file sitting in ~/.cloudflared/. Now you need to tell cloudflared what to do with incoming traffic. That's the job of config.yml.
Understanding the config.yml Structure
The default config file lives at ~/.cloudflared/config.yml. Cloudflared looks there automatically unless you tell it otherwise. The file has three required pieces: the tunnel ID, the path to the credentials file, and the ingress rules.
1tunnel: your-tunnel-id-here
2credentials-file: /home/youruser/.cloudflared/your-tunnel-id-here.json
3
4ingress:
5 - hostname: app.example.com
6 service: http://localhost:3000
7 - hostname: dashboard.example.com
8 service: http://192.168.1.50:8080
9 originRequest:
10 noTLSVerify: true
11 connectTimeout: 10s
12 - service: http_status:404The structure is deliberate. Cloudflare reads ingress rules top to bottom and routes traffic to the first match it finds. The final rule with no hostname is the mandatory catch-all. Without it, cloudflared refuses to start. It's not optional. It's not a suggestion.
Required: The Catch-All Rule
Every config.yml must end with a rule that has no hostname field. Use service: http_status:404 to return a clean 404 for unmatched requests. Omit it and cloudflared will throw a validation error before the tunnel ever connects.
Mapping Hostnames to Local Services with Ingress Rules
The app.example.com rule maps external traffic to http://localhost:3000, which is whatever process is running on that port locally. The dashboard.example.com rule points to http://192.168.1.50:8080, an internal server on your LAN. Cloudflared reaches that address directly from the machine running the tunnel.
The originRequest block handles per-ingress settings. noTLSVerify: true tells cloudflared to skip certificate validation when connecting to the origin. Use this when your internal service runs HTTPS with a self-signed cert. Without it, cloudflared will refuse the connection and log a certificate error. connectTimeout sets how long cloudflared waits before giving up on the origin.
noTLSVerify Is a Trust Decision
Setting noTLSVerify: true disables origin certificate validation. It's appropriate for internal services on networks you control. It's not appropriate for origins on the public internet or in environments with strict compliance requirements. Use it deliberately.
The traffic between your browser and Cloudflare's edge stays fully encrypted regardless of what noTLSVerify does on the origin side. The TLS that matters to your users is always present.
Step 6. Running the Tunnel and Testing Connectivity
The config file is written. The DNS CNAME records are in place from the previous step. Now you run the tunnel and confirm traffic actually flows.
Starting the Tunnel in Foreground Mode
cloudflared tunnel run <NAME>Replace <NAME> with the tunnel name you created. Cloudflared reads ~/.cloudflared/config.yml automatically. The log output starts immediately.
Watch the log carefully. Cloudflared establishes exactly four parallel connections to Cloudflare's edge network, each landing on a different data center. This is by design. If one connection drops, traffic continues over the remaining three without interruption.
A healthy startup log looks like this:
1INF Connection established connIndex=0 location=LAX
2INF Connection established connIndex=1 location=SEA
3INF Connection established connIndex=2 location=DFW
4INF Connection established connIndex=3 location=ORDFour Connection established lines. All green. A failed connection shows ERR with a reason. Common causes are network restrictions, firewall rules blocking outbound port 7844, or credential file mismatches.
Verifying Traffic Reaches Your Local Service
Open https://app.example.com in a browser. Your local app on port 3000 should respond. Open https://dashboard.example.com and confirm the internal server at 192.168.1.50:8080 responds as well.
From the command line:
curl -I https://app.example.comA successful response returns HTTP/2 200 plus whatever headers your app sends. A failed response returns a Cloudflare error page, which tells you whether the problem is DNS, the tunnel connection, or the origin service itself.
Use cloudflared tunnel info <NAME> to inspect live tunnel status, including which edge locations are connected and when the tunnel was last active. It's the fastest way to confirm the tunnel is alive without checking logs line by line.
Step 7. Running cloudflared as a System Service
Running cloudflared in a terminal works for testing. It doesn't survive a reboot. For a tunnel that stays up permanently, you install it as a system service.
Installing as a systemd Service on Linux
sudo cloudflared service installThat single command does several things at once. It copies the cloudflared binary to /usr/local/bin/, writes a systemd unit file to /etc/systemd/system/cloudflared.service, and registers the service with systemd. The unit file points to your config at ~/.cloudflared/config.yml by default.
Enable and start the service:
sudo systemctl enable cloudflared
sudo systemctl start cloudflaredCheck the status:
sudo systemctl status cloudflaredYou want active (running). Anything else means cloudflared failed to start. Check the logs with journalctl -u cloudflared -f to see why.
If your config file isn't at the default location, edit the unit file directly. Open /etc/systemd/system/cloudflared.service and add --config /path/to/your/config.yml to the ExecStart line. Run sudo systemctl daemon-reload afterward.
Running as a Service on macOS and Windows
On macOS with a Homebrew install, cloudflared uses launchd instead of systemd. Run sudo cloudflared service install from a terminal with admin rights. The command writes a plist file to /Library/LaunchDaemons/com.cloudflare.cloudflared.plist and loads it immediately. The tunnel starts at login and restarts automatically if it crashes.
On Windows, open PowerShell as Administrator and run:
cloudflared service installThis registers cloudflared as a Windows service. Open Services in the management console to confirm it appears and set it to start automatically.
Custom Config Path on Any Platform
If you manage multiple tunnels on the same machine, each with its own config file, pass the path explicitly: cloudflared tunnel --config /path/to/config.yml run. The service install command accepts the same flag. Keeping configs in named subdirectories prevents them from colliding.
Verifying the Service Starts on Boot
Reboot the machine. After it comes back up, run systemctl status cloudflared on Linux or check the Windows Services panel. The tunnel should be running without any manual intervention. If it isn't, the unit file or plist didn't register correctly, and the install step needs to be repeated with elevated permissions.
Troubleshooting Common Cloudflare Tunnel Errors
Most tunnel problems fall into three categories: the connection never establishes, traffic reaches the tunnel but doesn't route correctly, or TLS handshakes fail between cloudflared and the origin. Each category has distinct error messages and distinct fixes.
Connection and Authentication Errors
failed to sufficiently increase receive buffer size appears on Linux systems with low UDP buffer limits. Fix it:
sudo sysctl -w net.core.rmem_max=2500000Make it permanent by adding that line to /etc/sysctl.conf.
tunnel credentials file not found means the path in credentials-file inside config.yml doesn't match where the JSON file actually lives. Run ls ~/.cloudflared/ and confirm the filename. Paste the exact path into the config.
Error 1016 (Origin DNS error) means Cloudflare can't resolve the CNAME you created, or the record hasn't propagated yet. DNS propagation can take a few minutes after you create the CNAME in the dashboard. Check with dig app.example.com and confirm the CNAME points to your tunnel's .cfargotunnel.com address.
Increase Log Verbosity When Stuck
Add --loglevel debug to the run command for significantly more output. cloudflared tunnel --loglevel debug run <NAME> shows every connection attempt, every ingress match decision, and every origin request. It's verbose, but it surfaces the exact line where things break.
Ingress and Routing Mismatches
no ingress rules match means your config.yml is missing the catch-all rule. Add - service: http_status:404 as the final ingress entry. No hostname field. Just the service line.
Unable to reach the origin service means cloudflared connected to Cloudflare's edge fine but can't reach the local service you mapped. Check that the process is actually running on the port you specified. curl http://localhost:3000 from the same machine confirms whether the origin is alive independently of the tunnel.
TLS and Certificate Issues
certificate signed by unknown authority appears when cloudflared tries to connect to an HTTPS origin with a self-signed certificate. Add noTLSVerify: true under originRequest for that ingress rule, as covered in Step 5.
On Linux, follow live logs with:
journalctl -u cloudflared -fThat stream updates in real time as cloudflared processes connections. Pair it with --loglevel debug during active troubleshooting and you'll see exactly which ingress rule matched, which origin was contacted, and what the origin returned.
What You Built. And What Comes Next
Recap of the Full Tunnel Setup
Start to finish, here's what this guide covered: install cloudflared, authenticate with your Cloudflare account, create a named tunnel and receive its credentials file, create DNS CNAME records routing your hostnames to the tunnel, write a config.yml mapping those hostnames to local services, run cloudflared as a persistent system service, test connectivity from a browser and from the command line, and troubleshoot the errors most likely to appear along the way.
The security model underneath all of it is worth stating plainly. No ports are open on your machine. Your IP address is never exposed. Every connection originates outbound from cloudflared to Cloudflare's edge. An attacker scanning the internet finds nothing to probe.
One tunnel handles multiple hostnames. You don't need a separate tunnel for every service. Add ingress rules to the same config.yml and reload the service.
Where to Take Your Tunnel Next: Zero Trust Access Policies
The tunnel is infrastructure. What sits in front of it determines who can use it. Right now, anyone who knows the URL can reach dashboard.example.com. That's fine for testing. It's not fine for anything you'd call production.
Part 13 adds Cloudflare Access in front of the tunnel. That means requiring authentication before a user can reach dashboard.example.com at all. No credentials, no page. The tunnel stays exactly as you built it. Access layers on top without touching the tunnel configuration. That's where the Zero Trust model starts to take shape.