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.
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
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 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.
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.
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.
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.
Installing cloudflared on Linux, macOS, and Windows
Installation is straightforward on all three platforms. On Linux with apt:
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.debOn macOS with Homebrew:
brew install cloudflaredOn Windows with winget:
winget install --id Cloudflare.cloudflaredA 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:
cloudflared tunnel loginThis 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:
cloudflared tunnel create my-tunnelThis 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:
cloudflared tunnel --url http://localhost:3000Cloudflare 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:
1sudo cloudflared service install
2sudo systemctl enable cloudflared
3sudo systemctl start cloudflaredQuick 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:
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:404The 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.
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.
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:
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:404In 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.
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.
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:
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:404The 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:
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 runThe 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:
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: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.
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.
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
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.