If you self-host applications at home or in an office — a media server, a home automation dashboard, a development environment, a personal wiki — you have traditionally needed to open ports on your router, configure NAT rules, set up dynamic DNS, and hope your ISP does not block inbound connections. Each open port is a potential attack vector, and the configuration is fragile.
Cloudflare Tunnels eliminate all of that. You run a lightweight daemon (cloudflared) on your local machine that creates an outbound-only encrypted connection to Cloudflare’s edge network. Traffic flows from the internet through Cloudflare to your service, with no inbound ports opened on your firewall whatsoever.
This guide covers everything from installation to advanced configurations including Zero Trust access policies, SSH tunneling, and multi-service setups.
Why Cloudflare Tunnels?
No Port Forwarding Required
Traditional self-hosting requires forwarding ports 80/443 (and possibly others) through your router to your server. With Cloudflare Tunnels, your firewall can block all inbound connections. The cloudflared daemon initiates outbound connections to Cloudflare, and traffic is relayed back through those connections.
No Public IP Needed
Even if your ISP uses Carrier-Grade NAT (CGNAT) and you do not have a public IP address, Cloudflare Tunnels work perfectly. The connection is outbound from your network to Cloudflare.
Built-in DDoS Protection
All traffic passes through Cloudflare’s network, which provides automatic DDoS mitigation, rate limiting, and bot protection. Your home IP address is never exposed.
Free Tier
Cloudflare Tunnels are included in the free Cloudflare plan. You need a Cloudflare account and a domain managed by Cloudflare DNS, but there is no additional cost for the tunnel itself.
Automatic TLS
Cloudflare handles SSL/TLS certificates automatically. Your local service can run on plain HTTP, and Cloudflare terminates TLS at their edge, presenting a valid certificate to visitors.
How Cloudflare Tunnels Work
The architecture is straightforward:
- You install
cloudflaredon the machine running your services cloudflaredauthenticates with your Cloudflare accountcloudflaredestablishes persistent outbound connections (using HTTP/2 or QUIC) to Cloudflare’s nearest data centers- When a visitor requests
app.knowledgexchange.xyz, Cloudflare routes the request through the tunnel to your localcloudflaredinstance cloudflaredforwards the request to your local service (e.g.,http://localhost:8080)- The response travels back through the same tunnel
Visitor -> Cloudflare Edge (TLS) -> Tunnel -> cloudflared -> localhost:8080
Key Point: Your server never accepts inbound connections. All tunnel connections are initiated outbound by
cloudflared. This means even if someone knows your home IP address, they cannot reach your services directly.
Prerequisites
Before starting, you need:
- A Cloudflare account (free tier is sufficient)
- A domain name with DNS managed by Cloudflare (you can transfer an existing domain or register a new one through Cloudflare Registrar)
- A Linux, macOS, or Windows machine running the service you want to expose
- The service running and accessible on
localhost(e.g., a web app on port 8080)
Installing cloudflared
Debian/Ubuntu
# Add the Cloudflare GPG key and repository
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update
sudo apt-get install -y cloudflared
RHEL/CentOS/Fedora
# Add Cloudflare repository
sudo rpm --import https://pkg.cloudflare.com/cloudflare-main.gpg
curl -fsSL https://pkg.cloudflare.com/cloudflared-ascii.repo | sudo tee /etc/yum.repos.d/cloudflared.repo
sudo yum install -y cloudflared
macOS
brew install cloudflared
Windows
Download the installer from the Cloudflare GitHub releases or use winget:
winget install --id Cloudflare.cloudflared
Docker
docker pull cloudflare/cloudflared:latest
Verify Installation
cloudflared --version
# cloudflared version 2026.1.x (built ...)
Authenticating with Cloudflare
Before creating tunnels, authenticate cloudflared with your Cloudflare account:
cloudflared tunnel login
This opens a browser window where you select the domain you want to use with tunnels. After authorization, a certificate is saved to ~/.cloudflared/cert.pem.
Note: This step only needs to be done once per machine. The certificate is used for all future tunnel operations.
Creating a Tunnel
Create the Tunnel
cloudflared tunnel create my-homelab
This generates:
- A unique Tunnel ID (a UUID like
a1b2c3d4-e5f6-7890-abcd-ef1234567890) - A credentials file at
~/.cloudflared/<TUNNEL_ID>.json
Tunnel credentials written to /home/user/.cloudflared/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json
Created tunnel my-homelab with id a1b2c3d4-e5f6-7890-abcd-ef1234567890
List Your Tunnels
cloudflared tunnel list
Configure DNS
Route a subdomain to your tunnel:
cloudflared tunnel route dns my-homelab app.knowledgexchange.xyz
This creates a CNAME record in your Cloudflare DNS pointing app.knowledgexchange.xyz to <TUNNEL_ID>.cfargotunnel.com.
Configuring Ingress Rules
Create a configuration file at ~/.cloudflared/config.yml:
Single Service
tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /home/user/.cloudflared/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json
ingress:
- hostname: app.knowledgexchange.xyz
service: http://localhost:8080
- service: http_status:404
Important: The last ingress rule must be a catch-all (no
hostnamespecified). This handles requests that do not match any defined hostname. Usinghttp_status:404returns a 404 for unmatched requests.
Multiple Services on Subdomains
This is where Cloudflare Tunnels truly shine. A single tunnel can expose multiple services on different subdomains:
tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /home/user/.cloudflared/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json
ingress:
# Reverse proxy to a web application
- hostname: app.knowledgexchange.xyz
service: http://localhost:8080
# Home Assistant instance
- hostname: home.knowledgexchange.xyz
service: http://localhost:8123
# Gitea self-hosted Git server
- hostname: git.knowledgexchange.xyz
service: http://localhost:3000
# Grafana monitoring dashboard
- hostname: grafana.knowledgexchange.xyz
service: http://localhost:3001
# Jellyfin media server
- hostname: media.knowledgexchange.xyz
service: http://localhost:8096
# Catch-all rule (required)
- service: http_status:404
For each hostname, create a DNS route:
cloudflared tunnel route dns my-homelab app.knowledgexchange.xyz
cloudflared tunnel route dns my-homelab home.knowledgexchange.xyz
cloudflared tunnel route dns my-homelab git.knowledgexchange.xyz
cloudflared tunnel route dns my-homelab grafana.knowledgexchange.xyz
cloudflared tunnel route dns my-homelab media.knowledgexchange.xyz
Advanced Ingress Options
You can fine-tune each ingress rule with additional options:
ingress:
- hostname: app.knowledgexchange.xyz
service: http://localhost:8080
originRequest:
# Timeout for connecting to the local service
connectTimeout: 10s
# Disable TLS verification for self-signed certs on local services
noTLSVerify: true
# Forward the original Host header
httpHostHeader: app.knowledgexchange.xyz
# Keep-alive settings
keepAliveConnections: 100
keepAliveTimeout: 90s
- hostname: secure.knowledgexchange.xyz
service: https://localhost:8443
originRequest:
# If your local service uses HTTPS with a self-signed cert
noTLSVerify: true
- service: http_status:404
Running the Tunnel
Manual Start (for Testing)
cloudflared tunnel run my-homelab
You should see output indicating the tunnel is connected:
INF Starting tunnel tunnelID=a1b2c3d4-e5f6-7890-abcd-ef1234567890
INF Connection established connIndex=0 location=DFW
INF Connection established connIndex=1 location=IAH
INF Connection established connIndex=2 location=DFW
INF Connection established connIndex=3 location=IAH
Note:
cloudflaredestablishes multiple connections to different Cloudflare data centers for redundancy. If one connection drops, traffic is automatically routed through the remaining connections.
Validate Your Configuration
Before running, validate the configuration:
cloudflared tunnel ingress validate
Test which ingress rule matches a specific URL:
cloudflared tunnel ingress rule https://app.knowledgexchange.xyz
# Using rules from /home/user/.cloudflared/config.yml
# Matched rule #0: hostname=app.knowledgexchange.xyz service=http://localhost:8080
Running as a systemd Service
For production use, run cloudflared as a system service that starts automatically on boot:
Install the Service
sudo cloudflared service install
This creates a systemd service unit and copies your configuration to /etc/cloudflared/config.yml and your credentials to /etc/cloudflared/.
Manage the Service
# Start the service
sudo systemctl start cloudflared
# Enable auto-start on boot
sudo systemctl enable cloudflared
# Check status
sudo systemctl status cloudflared
# View logs
sudo journalctl -u cloudflared -f
Manual systemd Unit (Alternative)
If you prefer to create the service manually:
# /etc/systemd/system/cloudflared.service
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=on-failure
RestartSec=5s
User=cloudflared
Group=cloudflared
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/log/cloudflared
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
Dashboard Management
Cloudflare Tunnels can also be created and managed entirely from the Cloudflare Zero Trust dashboard:
- Log in to the Cloudflare Zero Trust Dashboard
- Navigate to Networks > Tunnels
- Click Create a Tunnel
- Follow the wizard to name the tunnel and get an installation command
- Configure public hostnames and services in the web UI
The dashboard approach generates a token-based connector command:
sudo cloudflared service install <TOKEN>
Tip: Dashboard-managed tunnels are recommended for teams because the configuration is stored centrally in Cloudflare, not in a local file on the server. Multiple team members can update routing rules without SSH access to the server.
Access Policies with Cloudflare Zero Trust
For services that should not be publicly accessible (admin panels, dashboards, internal tools), add Cloudflare Access policies:
Setting Up an Access Policy
- In the Zero Trust dashboard, go to Access > Applications
- Click Add an Application
- Choose Self-hosted
- Configure:
- Application name: Grafana Dashboard
- Session duration: 24 hours
- Application domain:
grafana.knowledgexchange.xyz
- Add a policy:
- Policy name: Allowed Users
- Action: Allow
- Include rule: Emails ending in
@knowledgexchange.xyz
- Save
Now, anyone accessing grafana.knowledgexchange.xyz will be presented with a Cloudflare Access login page. Only users with authorized email addresses can proceed.
Authentication Options
Cloudflare Access supports multiple identity providers:
- One-Time PIN (email) — no IdP required, Cloudflare sends a code to the user’s email
- Google Workspace
- Microsoft Azure AD / Entra ID
- GitHub / GitLab
- Okta, OneLogin, SAML, OIDC
SSH Through Cloudflare Tunnels
You can use Cloudflare Tunnels to provide secure SSH access without exposing port 22:
Server-Side Configuration
Add an SSH ingress rule in your config.yml:
ingress:
- hostname: ssh.knowledgexchange.xyz
service: ssh://localhost:22
- hostname: app.knowledgexchange.xyz
service: http://localhost:8080
- service: http_status:404
Route the DNS:
cloudflared tunnel route dns my-homelab ssh.knowledgexchange.xyz
Client-Side Configuration
On the client machine, install cloudflared and add this to ~/.ssh/config:
Host ssh.knowledgexchange.xyz
ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
Now you can SSH normally:
ssh user@ssh.knowledgexchange.xyz
The SSH connection is tunneled through Cloudflare. If you have an Access policy on ssh.knowledgexchange.xyz, the user will be prompted to authenticate through the browser before the SSH connection is established.
Short-Lived SSH Certificates
For even stronger security, configure Cloudflare to issue short-lived SSH certificates, eliminating the need for static SSH keys:
- Generate a CA key pair via the Zero Trust dashboard
- Configure your SSH server to trust the Cloudflare CA
- Users authenticate through Cloudflare Access and receive a temporary certificate
This approach means no SSH keys to manage, rotate, or revoke.
TCP and UDP Tunnels
Beyond HTTP and SSH, Cloudflare Tunnels support arbitrary TCP and UDP protocols:
TCP Example (Database Access)
ingress:
- hostname: db.knowledgexchange.xyz
service: tcp://localhost:5432 # PostgreSQL
- service: http_status:404
Client-side access:
cloudflared access tcp --hostname db.knowledgexchange.xyz --url localhost:5432
Then connect your database client to localhost:5432, and the traffic is tunneled to your remote PostgreSQL server.
UDP Example
ingress:
- hostname: dns.knowledgexchange.xyz
service: udp://localhost:53 # DNS server
- service: http_status:404
Monitoring and Metrics
Built-in Metrics
cloudflared exposes Prometheus metrics on port 60123 by default:
# In config.yml
metrics: localhost:60123
Access metrics at http://localhost:60123/metrics. Key metrics include:
cloudflared_tunnel_request_per_second— Request ratecloudflared_tunnel_response_by_code— HTTP response code distributioncloudflared_tunnel_concurrent_requests_per_tunnel— Active connections
Cloudflare Dashboard Monitoring
In the Zero Trust dashboard under Networks > Tunnels, you can see:
- Connection status (healthy/degraded/down)
- Connected data center locations
- Active connections count
- Tunnel uptime
Troubleshooting
Tunnel Not Connecting
# Check if cloudflared can reach Cloudflare
cloudflared tunnel run --loglevel debug my-homelab
# Verify your credentials file exists
ls -la ~/.cloudflared/
# Check DNS resolution
dig app.knowledgexchange.xyz CNAME
502 Bad Gateway Errors
This usually means cloudflared cannot reach your local service:
# Verify the local service is running
curl -v http://localhost:8080
# Check if the service is bound to localhost vs 0.0.0.0
ss -tlnp | grep 8080
Common Mistake: If your service is running in Docker with
-p 127.0.0.1:8080:8080, it is accessible fromcloudflaredon the host. But if you runcloudflaredin Docker too, they need to be on the same Docker network. Use Docker Compose to ensure both containers share a network.
Connection Drops
If connections are frequently dropping:
# Check system logs
sudo journalctl -u cloudflared --since "1 hour ago"
# Ensure your network allows outbound connections on ports 443 and 7844
# Port 7844 is used for QUIC connections
Permission Errors
# Ensure the credentials file is readable
chmod 600 ~/.cloudflared/*.json
# For systemd service, ensure /etc/cloudflared/ has correct permissions
sudo chown -R cloudflared:cloudflared /etc/cloudflared/
sudo chmod 700 /etc/cloudflared/
Summary
Cloudflare Tunnels fundamentally change how you expose self-hosted services to the internet. No more port forwarding, no more dynamic DNS, no more worrying about ISP restrictions or exposed IP addresses. The outbound-only connection model means your firewall stays locked down while your services remain accessible.
Combined with Cloudflare Zero Trust access policies, you get enterprise-grade authentication and authorization for your self-hosted applications — all on the free tier. Whether you are running a home lab, a small business server, or a development environment, Cloudflare Tunnels provide a secure, reliable, and free solution.
For related networking and security topics, see our articles on configuring Nginx and connecting to remote systems using SSH.