Image for Cloudflare Command Center: Domains, DNS, Zero Trust, and Tunnels from Beginner to Expert Part 12: Building Your First Cloudflare Tunnel Step by Step
Technology Aug 14, 2026 • 16 min read

Cloudflare Command Center: Domains, DNS, Zero Trust, and Tunnels from Beginner to Expert Part 12: Building Your First Cloudflare Tunnel Step by Step

Learn to build a Cloudflare Tunnel from scratch. Install cloudflared, route DNS, map hostnames to local services, and run it as a system service.

Share:
Lee Foropoulos

Lee Foropoulos

16 min read

Continue where you left off?
Text size:

Contents

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.

Modern server infrastructure with clean cable management
A Cloudflare Tunnel means your server never has to accept a connection it didn't initiate first.
98%
of Cloudflare Tunnel connections require zero inbound firewall rule changes

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 cloudflared daemon is the only piece of software that knows where your server actually lives. Cloudflare's edge doesn't need to know. Neither does the internet.

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.

Clean desk workspace with monitor displaying network architecture
The architecture is straightforward once you see the full path from browser to local service.

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.

2
example routing scenarios covered in this guide: app.example.com and dashboard.example.com

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:

bash
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 cloudflared

For RHEL, CentOS, and Fedora systems, use the RPM repository instead:

bash
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 cloudflared

If 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:

bash
cloudflared --version

Check the Cloudflare GitHub releases page if you need a specific version or want to confirm you're on the latest build.

Server rack with organized cabling and indicator lights
The install takes under a minute. The verification step takes five seconds. Do both.

Installation on macOS with Homebrew

One line:

bash
brew install cloudflared

Homebrew 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:

powershell
cloudflared --version

The 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:

bash
cloudflared tunnel login

Running 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:

bash
cloudflared tunnel create my-app-tunnel

The 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.

Abstract data visualization with glowing connection nodes
Every tunnel gets a UUID at creation. That identifier persists for the lifetime of the tunnel.
1
credentials file created per tunnel, scoped only to that tunnel's operations
The tunnel UUID is the stable identity. Names are for humans. The UUID is what Cloudflare's edge actually routes traffic through.

Confirm the tunnel exists:

bash
cloudflared tunnel list

This 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:

bash
cloudflared tunnel route dns my-app-tunnel app.example.com

Cloudflare 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.

DNS dashboard interface showing domain records and configuration options
The CNAME Cloudflare creates points to cfargotunnel.com, not to your server. Your server's address never enters DNS.

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.

A grey-cloud CNAME on a tunnel record doesn't just reduce security. It breaks the tunnel entirely, because the DNS-only record can't route traffic through Cloudflare's edge.

Example: app.example.com and dashboard.example.com

Route the second hostname the same way:

bash
cloudflared tunnel route dns my-app-tunnel dashboard.example.com

Both 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.

yaml
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:404

The 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.

1
catch-all rule required per config.yml. No exceptions

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

bash
cloudflared tunnel run <NAME>

Replace <NAME> with the tunnel name you created. Cloudflared reads ~/.cloudflared/config.yml automatically. The log output starts immediately.

Four connections. Every time. Cloudflare doesn't trust a single path, and neither should you.

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.

4
parallel edge connections cloudflared maintains per tunnel for redundancy

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=ORD

Four 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:

bash
curl -I https://app.example.com

A 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

bash
sudo cloudflared service install

That 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.

A terminal window showing systemd service status output with active running state
A clean `active (running)` status in systemctl means cloudflared survived the install and is maintaining its edge connections.

Enable and start the service:

bash
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

Check the status:

bash
sudo systemctl status cloudflared

You 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:

powershell
cloudflared service install

This 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:

bash
sudo sysctl -w net.core.rmem_max=2500000

Make 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.

The tunnel isn't the problem. The tunnel is almost never the problem. Check your origin first.

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.

A developer reviewing terminal log output on a dark-themed code editor
Reading cloudflared logs methodically is faster than guessing. The error messages are specific once you know what to look for.

On Linux, follow live logs with:

bash
journalctl -u cloudflared -f

That 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.

0
inbound ports required to expose services through a Cloudflare Tunnel

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.

Next Steps After Your First Tunnel 0/4

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.

How was this article?

Share

Link copied to clipboard!

You Might Also Like

Lee Foropoulos

Lee Foropoulos

Business Development Lead at Lookatmedia, fractional executive, and founder of gotHABITS.

🔔

Never Miss a Post

Get notified when new articles are published. No email required.

You will see a banner on the site when a new post is published, plus a browser notification if you allow it.

Browser notifications only. No spam, no email.

0 / 0