Image for Cloudflare Command Center: Domains, DNS, Zero Trust, and Tunnels from Beginner to Expert Part 11: Cloudflare Tunnel. Securely Expose Services Without Opening Ports
Technology Aug 07, 2026 • 17 min read

Cloudflare Command Center: Domains, DNS, Zero Trust, and Tunnels from Beginner to Expert Part 11: Cloudflare Tunnel. Securely Expose Services Without Opening Ports

Learn how Cloudflare Tunnel lets you expose local apps, home labs, and staging servers to the internet without opening a single firewall port.

Share:
Lee Foropoulos

Lee Foropoulos

17 min read

Continue where you left off?
Text size:

Contents

Part 10 built out the DNS architecture that makes everything else in this series possible. You configured records, understood proxy modes, and got a clear picture of how Cloudflare sits between the internet and your infrastructure. That foundation matters here, because this part assumes you already have a domain pointed at Cloudflare. If you don't, go back to Part 10 and get that sorted first.

This part is about tunnels. Specifically, Cloudflare Tunnel, which is the answer to a question every homelab operator and solo developer eventually asks: how do I make this thing publicly accessible without turning my network into a liability? The answer most people reach for is port forwarding. That answer is wrong, and this part explains why before showing you something better.

By the end of this part, you'll have a working mental model of how Cloudflare Tunnel operates, why it eliminates entire categories of risk, and how to take a service running on your local machine and expose it to the internet without opening a single inbound port on your firewall. That's not a trick. It's a fundamentally different approach to network exposure, and once you understand it, you won't go back.

The Problem With Exposing Services the Old Way

Why Open Ports Are a Liability

Port forwarding works. That's the honest thing to say about it. You open a port on your router, point it at a machine on your local network, and traffic reaches your service. Developers have been doing this for decades. It solves the immediate problem of accessibility. It also creates a cluster of new problems that most people don't think about until something goes wrong.

The first problem is visibility. The moment you open an inbound port, that port is discoverable. Tools like Shodan index exposed services continuously. Automated scanners probe every IPv4 address on the internet on a rotating basis. Your home IP, your VPS, your staging server: if a port is open, it will be found. The question isn't whether someone will find it. The question is what they'll do when they do.

56%
of all internet traffic is automated bots and scanners, not human users

The second problem is your IP address itself. Port forwarding exposes your real IP directly in DNS records. Anyone who resolves your domain gets your actual address. From there, they can target your connection directly, run volumetric attacks, or attempt to exploit services that sit behind whatever thin layer of authentication you've put in front of them.

The Attack Surface You Didn't Know You Had

Network cables and server hardware representing internet infrastructure
Every open inbound port is a door. Not all of them have locks good enough to matter.

Home labs and small staging servers almost never have enterprise perimeter protection. There's no intrusion prevention system sitting upstream. There's no dedicated DDoS mitigation layer. There's a router with firmware that may or may not have been updated recently, and whatever software is running on the exposed machine.

Dynamic IPs compound the problem. Many residential ISPs rotate your IP address periodically, which means your DNS records go stale, your service becomes unreachable, and you either set up a dynamic DNS updater or accept the downtime. Some ISPs block inbound connections on common ports entirely, which means you're fighting your own provider before you've even started.

TLS encrypts the data in transit. It does nothing to stop someone from hammering your server directly with connection attempts until something breaks.

TLS is not a perimeter defense. It protects the content of a connection, not the connection itself. A service with a valid certificate is still fully exposed to brute force, denial-of-service, and direct exploitation of any vulnerability in the application layer. The certificate just means the attacker's traffic is encrypted on its way to your server.

Cloudflare Tunnel exists to solve all of this at once. No open ports. No exposed IP. No direct path from the internet to your machine.

What Is a Cloudflare Tunnel?

The Core Concept in Plain English

A Cloudflare Tunnel flips the connection model entirely. Instead of your router accepting inbound connections and forwarding them to your server, your server reaches outward to Cloudflare's edge and holds that connection open. All traffic then flows through that outbound connection. Your firewall never needs an inbound rule. Your IP never appears in DNS. The internet sees Cloudflare's infrastructure, not yours.

The component that makes this work is cloudflared, a small daemon that runs on your machine or server. It initiates and maintains encrypted outbound connections to the nearest Cloudflare data center using QUIC or HTTP/2. When a user requests your domain, Cloudflare's edge receives that request, finds the associated tunnel, and forwards the traffic through the established connection to your local service. The response travels back the same way.

Laptop displaying code and network configuration
The cloudflared daemon runs quietly in the background, maintaining the outbound connection that makes your service reachable without exposing a single port.

The traffic path looks like this: a user's browser makes a request to your domain, that request hits Cloudflare's edge network, Cloudflare routes it through the tunnel to the cloudflared process on your machine, cloudflared forwards it to your local service on whatever port it's listening on, and the response travels back through the same path. Your machine never accepted an inbound connection from the internet directly.

Your server talks to Cloudflare. Cloudflare talks to the world. Your server never talks to the world directly.

How It Differs From a VPN or Reverse Proxy

The comparison to a VPN comes up often, and it's worth being precise about the difference. A VPN creates a network-level connection between a user's device and your network. The user becomes part of your network, with access to resources based on routing rules. That's a user-to-network model. Cloudflare Tunnel is a service-to-internet model. It exposes specific services to specific public hostnames, not your entire network to a set of authenticated users. The two tools solve different problems.

A traditional reverse proxy like NGINX or Caddy is closer in concept, but still requires an open inbound port. The proxy sits in front of your services and routes requests based on hostnames or paths, but the proxy itself is reachable from the internet on port 80 or 443. Someone can still target the proxy directly. Cloudflare Tunnel removes that exposure entirely.

Tunnel Is Part of Zero Trust

Cloudflare Tunnel isn't a standalone product. It's a component of Cloudflare's Zero Trust platform, which means it can be combined with Cloudflare Access to enforce identity-based authentication before traffic even reaches your service. That combination is covered in a later part of this series.

Tunnel also works for more than HTTP. Any TCP or UDP service can be exposed through a tunnel, including SSH, databases, and custom protocol services. The HTTP use case is the most common, but the capability is broader than most people realize.

Why Tunnels Dramatically Reduce Your Attack Surface

No Inbound Firewall Rules Required

This is the part that feels too good to be true the first time you encounter it. Your firewall can have zero inbound rules. None. The cloudflared daemon opens outbound connections, and all traffic flows through those connections. A port scanner targeting your IP address finds nothing. There are no open ports to enumerate, no services to fingerprint, no banners to read. From the perspective of anyone probing your network from the outside, that machine doesn't appear to be running anything accessible.

45 billion
automated bot requests blocked by Cloudflare's network on an average day

That's not an abstraction. It's a concrete reduction in the number of things that can go wrong. Brute force attacks require a reachable endpoint. Exploit attempts require a reachable service. If neither exists from the internet's perspective, those attack categories simply don't apply to your infrastructure.

Your Origin IP Stays Hidden

When you use Cloudflare's proxy mode with traditional DNS records, your origin IP is hidden from most users, but it can sometimes be discovered through historical DNS records, misconfigured services, or email headers. With Cloudflare Tunnel, the origin IP is structurally hidden. The DNS record for your tunnel hostname points to a Cloudflare-owned address associated with your tunnel's UUID. There's no A record pointing at your server. There's no way to resolve your domain and get your actual IP address.

"If an attacker can't find your origin IP, they can't bypass Cloudflare to attack you directly. Hiding the origin isn't a nice-to-have. It's the whole game."

This matters because a common attack pattern against Cloudflare-protected sites is finding the origin IP through other means and then sending traffic directly to it, bypassing Cloudflare's DDoS protection and WAF entirely. Tunnel eliminates that path by design.

Cloudflare Absorbs the Threat Layer

All traffic passing through a Cloudflare Tunnel flows through Cloudflare's edge before it reaches your service. That means DDoS mitigation, the Web Application Firewall, bot management, and rate limiting all apply to your tunneled service automatically, based on whatever Cloudflare plan you're on.

Access Policies Make This Even Stronger

Combining Cloudflare Tunnel with Cloudflare Access means identity verification happens at the edge before traffic ever enters the tunnel. An attacker can't even attempt to reach your service without passing an authentication check that your origin machine never has to handle.

Tunnel connections are mutually authenticated using short-lived certificates that cloudflared manages automatically. You don't configure these manually. The daemon handles certificate rotation, connection health monitoring, and automatic reconnection if a data center connection drops. Cloudflare runs multiple tunnel connections simultaneously to different edge locations for redundancy, so a single data center issue doesn't take your service offline.

The combined effect is a service that's publicly accessible through a clean hostname, protected by Cloudflare's full network layer, with an origin that's structurally invisible to anyone trying to attack it directly.

Meet cloudflared: The Daemon That Makes It All Work

What cloudflared Actually Does

cloudflared is the open-source connector that lives on your machine and does the actual work of maintaining the tunnel. It's maintained by Cloudflare, the source code is publicly available, and it's the only piece of software you install locally to make tunnels function. Everything else, the routing, the certificate management, the DNS records, is configured through Cloudflare's dashboard or API.

When cloudflared starts, it opens outbound connections to the nearest Cloudflare data center using QUIC by default, falling back to HTTP/2 if QUIC is unavailable. It authenticates using credentials tied to your Cloudflare account, establishes the tunnel, and then sits there waiting for traffic to forward. When a request arrives at Cloudflare's edge for your tunnel hostname, cloudflared receives it through the established connection and forwards it to the local service address you've configured.

Server hardware and networking equipment in a data center
cloudflared runs on your hardware and handles the outbound connection to Cloudflare's edge. The data center infrastructure on Cloudflare's side handles everything else.

Installing cloudflared on Linux, macOS, and Windows

Installation is straightforward on all three platforms. On Linux with apt:

bash
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

On macOS with Homebrew:

bash
brew install cloudflared

On Windows with winget:

bash
winget install --id Cloudflare.cloudflared

A Docker image is also available at cloudflare/cloudflared, which is useful for containerized environments where you'd rather not install a binary directly on the host.

After installation, authenticate cloudflared with your Cloudflare account:

bash
cloudflared tunnel login

This opens a browser window where you authorize the connection and select the domain you want to use. cloudflared stores a certificate file locally that it uses for subsequent authentication.

To create a named tunnel:

bash
cloudflared tunnel create my-tunnel

This generates two key artifacts: a credentials file (a JSON file containing the tunnel's secret, stored in ~/.cloudflared/) and a tunnel UUID that you'll reference in your configuration. The config.yml file you create manually is the second artifact, and together these two files define everything about how your tunnel behaves.

For quick testing without any of this setup, a single command works:

bash
cloudflared tunnel --url http://localhost:3000

Cloudflare assigns a random subdomain under trycloudflare.com and your local service is immediately accessible. Nothing persists after you stop the process.

Running cloudflared as a System Service

For a persistent tunnel that survives reboots, install cloudflared as a system service:

bash
1sudo cloudflared service install
2sudo systemctl enable cloudflared
3sudo systemctl start cloudflared

Quick Run vs. Named Tunnel

The quick-run mode (cloudflared tunnel --url) is great for testing and demos. Named tunnels with config.yml are what you use for anything that needs to stay up reliably. The credentials file for a named tunnel doesn't expire the way a quick-run session does, and the tunnel configuration survives cloudflared restarts.

On macOS, cloudflared integrates with launchd. On Windows, it installs as a Windows Service. The service approach is the right choice for any tunnel you intend to run continuously, because it means your service stays reachable even after a server restart without any manual intervention.

Public Hostname Routing: Mapping Domains to Local Services

How Ingress Rules Work in config.yml

The config.yml file is where you define the relationship between public hostnames and local services. It lives in ~/.cloudflared/ by default, and cloudflared reads it at startup. The ingress block is the section that does the routing work.

A minimal config.yml for a named tunnel looks like this:

yaml
1tunnel: your-tunnel-uuid-here
2credentials-file: /home/youruser/.cloudflared/your-tunnel-uuid-here.json
3
4ingress:
5  - hostname: app.example.com
6    service: http://localhost:3000
7  - hostname: dashboard.example.com
8    service: http://localhost:8080
9  - service: http_status:404

The last rule is required. It's the catch-all that handles any request that doesn't match a defined hostname. Without it, cloudflared will refuse to start. The http_status:404 response tells cloudflared to return a 404 for unmatched requests rather than crashing or behaving unpredictably.

Multiple monitors displaying code and configuration files at a developer workstation
A single config.yml can route multiple hostnames to multiple local services through a single tunnel instance.

Routing Multiple Services Through a Single Tunnel

One tunnel instance handles as many hostnames as you need. There's no practical limit that you'll hit in a homelab or small production environment. Each entry in the ingress block maps a hostname to a service address, and cloudflared evaluates them in order, top to bottom, stopping at the first match.

One tunnel, one daemon, one config file. Ten services, ten hostnames, zero open ports.

The service address doesn't have to be localhost. It can be any address reachable from the machine running cloudflared, including other machines on your local network. http://192.168.1.50:8080 is a valid service address if cloudflared can reach that IP. This means you can run cloudflared on one machine and route traffic to services running on other machines in your network, which is useful for homelabs with multiple servers.

Cloudflare automatically provisions and renews TLS certificates for every hostname in your tunnel configuration. You don't manage certificates. You don't run Certbot. You don't think about certificate expiration. Cloudflare handles the entire certificate lifecycle for tunneled hostnames.

Wildcard and Path-Based Routing

Path-based routing lets you send different URL paths to different local services through a single hostname:

yaml
1ingress:
2  - hostname: app.example.com
3    path: /api
4    service: http://localhost:4000
5  - hostname: app.example.com
6    service: http://localhost:3000
7  - service: http_status:404

In this configuration, requests to app.example.com/api go to the API service on port 4000, and everything else on that hostname goes to the frontend on port 3000. The order of rules matters here. The

Real-World Example 2: Exposing a Home Lab Dashboard and Staging Server

Home Lab Use Case: Proxmox, Portainer, or Grafana

Home lab dashboards are inconvenient by default. Proxmox runs on port 8006. Portainer sits on 9000. Grafana defaults to 3000. None of those are accessible outside your local network without either a VPN, a port forward, or a firewall rule you'll eventually forget about and regret. Cloudflare Tunnel changes that equation entirely.

73%
of self-hosted services exposed via open ports have no authentication layer beyond the app itself

With a named tunnel and two lines of ingress config, you can reach your Proxmox interface at proxmox.yourdomain.com from anywhere. No VPN client to install. No dynamic DNS to babysit. No NAT traversal headaches. The tunnel handles the connection from the inside out, and Cloudflare's network handles the HTTPS termination on the public side.

The real power comes when you layer Cloudflare Access on top. Access policies sit in front of the tunnel hostname and require authentication before a request ever reaches your dashboard. You configure it once in the Zero Trust dashboard, link it to your Google or GitHub identity provider, and your Proxmox login page is now behind SSO. Access policies get their own dedicated coverage later in this series, but the integration point is worth naming here: the tunnel exposes the service, and Access controls who gets through the door.

Staging Server Use Case: Share a Work-in-Progress With a Client

Sharing a feature branch with a client is one of those problems that has historically produced bad solutions. Credentials over Slack. Screenshots instead of live previews. VPN access granted to someone who doesn't know what a VPN is. Cloudflare Tunnel makes this clean.

A staging URL that just works, over HTTPS, with no setup on the client's end, is worth more than a demo you have to narrate around technical friction.

Run your feature branch locally or on a dev machine, point a tunnel ingress rule at it, and hand the client a URL like staging.yourdomain.com. They click it in a browser. That's the entire process on their end.

Here's a two-service config.yml covering both scenarios:

yaml
1tunnel: your-tunnel-id
2credentials-file: /root/.cloudflared/your-tunnel-id.json
3
4ingress:
5  - hostname: proxmox.yourdomain.com
6    service: https://192.168.1.10:8006
7    originRequest:
8      noTLSVerify: true
9  - hostname: staging.yourdomain.com
10    service: http://localhost:3001
11  - service: http_status:404

The catch-all rule at the bottom is required. Without it, unmatched requests cause errors instead of clean 404 responses. The noTLSVerify flag on the Proxmox entry handles the self-signed certificate Proxmox uses internally. Access policies layer on top of either or both hostnames without any changes to this file.


Running cloudflared in Docker

Why Docker Is the Preferred Deployment for Containers

If your home lab or server environment already runs containers, adding cloudflared as another container is the natural fit. You don't need to install anything on the host directly. You don't need to manage a systemd service. The tunnel lives inside your container stack, restarts with it, and references your other services by their Docker network names rather than IP addresses.

Docker DNS Makes Config Simple

Inside a Docker network, containers resolve each other by service name. If your web app is named webapp in your Compose file, the tunnel config can reference it as http://webapp:3000 without knowing or caring what IP Docker assigned it.

That last point matters more than it might seem. Static IPs inside Docker networks can shift when containers restart. Service names don't. Writing http://webapp:3000 in your config.yml is more reliable than writing http://172.18.0.4:3000 and hoping it stays accurate.

Docker Run Command and Docker Compose Example

The official image is cloudflare/cloudflared. A minimal docker run command looks like this:

bash
1docker run -d \
2  --name cloudflared \
3  --restart always \
4  -v /home/user/.cloudflared:/etc/cloudflared \
5  cloudflare/cloudflared:latest \
6  tunnel --config /etc/cloudflared/config.yml run

The volume mount brings your credentials file and config.yml into the container. The --restart always policy means the tunnel comes back up automatically after a host reboot or a container crash. That's the detail people forget until they come back from a weekend and find the tunnel has been down since Friday.

For a Compose-based setup, add cloudflared as a sidecar alongside your application:

yaml
1services:
2  webapp:
3    image: your-app-image
4    networks:
5      - tunnel-net
6
7  cloudflared:
8    image: cloudflare/cloudflared:latest
9    restart: always
10    command: tunnel --config /etc/cloudflared/config.yml run
11    volumes:
12      - ./cloudflared:/etc/cloudflared
13    networks:
14      - tunnel-net
15
16networks:
17  tunnel-net:
Server rack with networking equipment and cable management in a data center environment
The cloudflared container sits alongside your application containers and references them by service name over the shared Docker network.

Both containers share tunnel-net, so cloudflared can reach webapp by name. The credentials directory mounts from a local ./cloudflared folder, which keeps your tunnel credentials out of the image itself and easy to manage separately.


Monitoring Tunnel Health and Troubleshooting Common Issues

Checking Tunnel Status in the Dashboard

The Zero Trust dashboard gives you a live view of every tunnel associated with your account. Each tunnel entry shows its current status: healthy, degraded, or inactive. Healthy means at least one connector is active and passing traffic. Degraded means connections are dropping or reconnecting. Inactive means nothing is running.

Person reviewing monitoring dashboards on multiple screens in a network operations context
The Zero Trust dashboard surfaces connector count, cloudflared version, and the Cloudflare data center each connector is attached to.

Click into a tunnel and you see the active connector list, the version of cloudflared running on each connector, and which Cloudflare point of presence that connector has established its outbound connection to. If you see connectors you don't recognize, that's worth investigating. An orphaned connector from a decommissioned server sitting in that list is a signal that your cleanup process has gaps.

Cloudflare runs four parallel connections per tunnel by default. That's not a configuration you set; it's built into how cloudflared operates. If one connection drops, traffic fails over to the remaining three while the client reconnects. The retry logic is exponential backoff with jitter, which means reconnection attempts don't hammer the endpoint simultaneously after a network interruption.

Reading cloudflared Logs

The cloudflared process logs connection state, ingress rule matches, and error codes to stdout by default. If you're running it as a Docker container, docker logs cloudflared surfaces everything. If you're running it as a systemd service, journalctl -u cloudflared -f gives you a live stream.

From the CLI, cloudflared tunnel info <name> returns tunnel metadata including the tunnel ID, creation date, and connector history without needing to open the dashboard.

Common Errors and How to Fix Them

The Three Errors That Account for Most Tunnel Failures

Wrong credentials path: The credentials-file value in config.yml points to a file that doesn't exist at that path inside the running process. Double-check the path, especially in Docker where the host path and container path differ after the volume mount.

Local service not running: The tunnel is healthy but the service it's proxying to is down. The tunnel will return a 502 to the client. Start the local service first, then verify the tunnel can reach it.

Missing catch-all ingress rule: Every config.yml must end with a catch-all entry. Without it, requests that don't match a hostname rule produce an error instead of a clean response.

All three of these are configuration issues, not tunnel issues. The tunnel itself is almost never the problem.


Security Best Practices for Cloudflare Tunnels

Least Privilege: Only Expose What You Need

A tunnel is not a reason to expose everything. The fact that you can route twenty services through a single tunnel doesn't mean you should. Scope each tunnel to a single purpose or a single application group. If a tunnel's credentials are compromised, the blast radius should be small.

Every service you expose without an Access policy in front of it is a service you're trusting the internet not to abuse. That's not a bet worth taking on an admin interface.

Admin interfaces deserve particular attention. Proxmox, Portainer, Grafana with write access, any internal tool with privileged actions: none of these should be reachable via tunnel without Cloudflare Access in front of them. The tunnel handles transport. Access handles identity. Neither one substitutes for the other.

Store your credentials files like API keys. That means not committing them to a public repository, not leaving them world-readable on a shared host, and not embedding them in container images. Treat a leaked credentials file as a security incident, not a configuration inconvenience.

Rotate Credentials and Audit Access Regularly

"Security configurations that never get reviewed are configurations that accumulate risk quietly until something breaks loudly."

Tunnel credentials should be rotated on a schedule, and that schedule should shorten whenever a team member leaves or a server is decommissioned. Cloudflare's audit log tracks tunnel creation, deletion, and configuration changes with timestamps and actor identities. Check it. Knowing who created a tunnel and when is useful information when you're trying to understand your exposure.

Monitor the connector list in the Zero Trust dashboard for connectors that shouldn't be there. A connector tied to a server you retired three months ago is either a ghost record or a sign that something is still running somewhere that you thought was off.

Running cloudflared as a non-root user or inside a restricted container reduces the damage ceiling if the process is ever exploited. It's a small operational change with a meaningful security return.


Your Cloudflare Tunnel Setup Checklist

Cloudflare Tunnel Setup Checklist 0/11

Part 10 covered Cloudflare Access policies and identity provider configuration, which is the authentication layer that makes everything in this checklist meaningfully secure. If you skipped it, the Access steps in this list won't make full sense until you go back. The tunnel gets you connectivity. Access gets you control.

Part 12 moves into Cloudflare Workers: what they are, how they execute at the edge, and how to use them to handle routing logic, header manipulation, and lightweight API responses without spinning up a server at all.

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