← All articles
NETWORKING Cloudflare Tunnels: Zero-Trust Access to Your Self-H... 2026-02-09 · cloudflare · tunnels · zero-trust

Cloudflare Tunnels: Zero-Trust Access to Your Self-Hosted Services

Networking 2026-02-09 cloudflare tunnels zero-trust networking remote-access security

The traditional way to expose a self-hosted service to the internet goes like this: open a port on your router, set up dynamic DNS, configure a reverse proxy, get TLS certificates, and hope nobody finds your open port before you've locked everything down properly.

Cloudflare Tunnels flips this model. Instead of opening inbound ports, you run a small daemon (cloudflared) on your server that creates an outbound connection to Cloudflare's network. Traffic flows through Cloudflare to your service, and your home IP address is never exposed. No port forwarding, no dynamic DNS, no inbound firewall rules.

How It Works

The architecture is straightforward:

User → Cloudflare Edge → Tunnel → cloudflared (your server) → Your Service
  1. cloudflared runs on your server and establishes an outbound connection to Cloudflare
  2. You configure DNS records in Cloudflare to point your domain at the tunnel
  3. When someone visits jellyfin.yourdomain.com, Cloudflare routes the request through the tunnel to your server
  4. Your server processes the request and sends the response back through the tunnel

Your router's firewall stays completely closed. No port 80, no port 443, no port forwarding rules at all. The only outbound connection is from cloudflared to Cloudflare's network.

Why This Matters for Self-Hosters

No exposed home IP

When you port-forward, anyone who resolves your domain sees your home IP address. With Cloudflare Tunnels, DNS resolves to Cloudflare's IPs. Your actual IP is hidden behind their network.

No port forwarding

Your router doesn't need any inbound rules opened. This eliminates an entire category of misconfiguration risks — you can't accidentally expose an admin panel if there are no open ports.

Automatic TLS

Cloudflare handles TLS termination at their edge. You get HTTPS with valid certificates without running Let's Encrypt or managing certificate renewals.

Works behind CGNAT

If your ISP uses Carrier-Grade NAT (common with 5G/LTE home internet and some fiber providers), you literally cannot port-forward. Cloudflare Tunnels work because the connection is outbound from your server.

Cloudflare Tunnels vs. Alternatives

Feature Cloudflare Tunnels Tailscale Funnel ngrok Pangolin
Price Free (generous) Free (limited) Free (limited) Free (self-hosted)
Custom domains Yes (your domain on Cloudflare) *.ts.net subdomains Paid plans only Yes
IP hiding Yes Yes Yes Depends on setup
Port forwarding needed No No No No
Access policies Yes (Cloudflare Access) Tailscale ACLs IP restrictions (paid) Basic auth
DDoS protection Yes (Cloudflare's network) No No No
CDN/caching Yes No No No
Bandwidth limits None on free tier Limited 1 GB/mo free Unlimited (self-hosted)
Self-hosted option No (SaaS) Partial (Headscale) No Yes
WebSocket support Yes Yes Yes Yes
TCP/UDP tunnels Yes (paid for arbitrary TCP) Yes Yes (paid) Yes
Setup complexity Low-medium Very low Very low Medium

Choose Cloudflare Tunnels when

Choose Tailscale Funnel when

Choose ngrok when

Choose Pangolin when

Setting Up Cloudflare Tunnels

Prerequisites

Step 1: Create a tunnel

You can create tunnels through the Cloudflare dashboard (Zero Trust > Networks > Tunnels) or via the CLI. The dashboard method is simpler for getting started:

  1. Go to Cloudflare Zero Trust dashboard
  2. Navigate to Networks > Tunnels
  3. Click Create a tunnel
  4. Name it (e.g., "homelab")
  5. Cloudflare generates a tunnel token

Step 2: Run cloudflared

Using Docker:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=your-tunnel-token-here
    restart: unless-stopped
    # If routing to containers on the same Docker network:
    networks:
      - homelab

Or install directly on the host:

# Debian/Ubuntu
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg
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 update && sudo apt install cloudflared

# Run as a service
sudo cloudflared service install your-tunnel-token-here

On Fedora or RHEL-based systems:

sudo dnf install cloudflared
sudo cloudflared service install your-tunnel-token-here

Step 3: Configure routes

Back in the Cloudflare dashboard, add public hostnames to your tunnel. Each hostname maps to a local service:

Public hostname Service URL
jellyfin.yourdomain.com HTTP http://localhost:8096
grafana.yourdomain.com HTTP http://localhost:3000
nextcloud.yourdomain.com HTTP http://localhost:8080

If cloudflared runs in Docker and your services are on a Docker network, use the container name instead of localhost:

Public hostname Service URL
jellyfin.yourdomain.com HTTP http://jellyfin:8096

Cloudflare automatically creates the CNAME DNS records for you.

Step 4: Verify

Visit https://jellyfin.yourdomain.com — you should see your Jellyfin instance with a valid Cloudflare-issued TLS certificate. No port forwarding configured, no Let's Encrypt, no reverse proxy.

Configuration File Approach

Instead of the dashboard, you can manage tunnel config as code. Create a config.yml:

tunnel: your-tunnel-id
credentials-file: /etc/cloudflared/credentials.json

ingress:
  - hostname: jellyfin.yourdomain.com
    service: http://localhost:8096
  - hostname: grafana.yourdomain.com
    service: http://localhost:3000
  - hostname: nextcloud.yourdomain.com
    service: http://localhost:8080
    originRequest:
      noTLSVerify: true  # If the origin uses self-signed certs
  # Catch-all rule (required)
  - service: http_status:404
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    command: tunnel run
    volumes:
      - ./config.yml:/etc/cloudflared/config.yml
      - ./credentials.json:/etc/cloudflared/credentials.json
    restart: unless-stopped

The config-file approach is better for version control and reproducibility. The dashboard approach is better for quick changes and initial setup.

Note: You can use dashboard-managed tunnels (token-based) or locally-managed tunnels (config file). They work differently and can't be mixed for the same tunnel. Dashboard-managed is newer and generally recommended for simplicity.

Access Policies with Cloudflare Access

Here's where Cloudflare Tunnels get really powerful. You can put authentication in front of any tunneled service, even services that have no built-in auth:

  1. In Zero Trust dashboard, go to Access > Applications
  2. Add a new application
  3. Set the domain (e.g., grafana.yourdomain.com)
  4. Create a policy defining who can access it

Authentication methods

Cloudflare Access supports multiple identity providers:

Example policy: email-based access

Application: grafana.yourdomain.com
Policy: Allow
  - Include: Email ends with @yourdomain.com
  - Include: Email is [email protected]

Anyone visiting grafana.yourdomain.com sees a Cloudflare login page. They enter their email, receive a one-time PIN, and get access. No accounts to create, no passwords to manage.

Example policy: GitHub org access

Application: admin-tools.yourdomain.com
Policy: Allow
  - Include: GitHub Organization is "your-github-org"

Only members of your GitHub organization can access the service. This is excellent for team homelab setups or shared infrastructure.

Bypass for specific services

Some services don't work well with an auth page in front of them (APIs, webhooks, mobile apps). You can bypass Access for specific paths:

Application: nextcloud.yourdomain.com
Policy: Bypass
  - Path: /remote.php/*
  - Path: /ocs/*
Policy: Allow
  - Include: Email is [email protected]

This lets Nextcloud's mobile app and CalDAV/CardDAV clients connect directly while still requiring auth for browser access.

Free Tier vs. Paid Features

Free (Zero Trust free plan — up to 50 users)

Paid (Zero Trust paid plans)

For most self-hosters, the free tier is more than enough. You hit the paid wall when you need TCP tunnels for non-HTTP services (like SSH access through the tunnel) or when you have more than 50 users accessing your services.

The WARP client workaround for TCP

You can access TCP services (SSH, RDP) through Cloudflare Tunnels on the free tier if users install the WARP client with your Zero Trust organization configured. WARP routes traffic through your tunnel without needing the paid TCP tunnel feature. The trade-off is that every user needs WARP installed and configured.

Practical Considerations

Latency

Your traffic goes: user -> Cloudflare edge -> tunnel -> your server -> tunnel -> Cloudflare edge -> user. This adds some latency compared to a direct connection. For web applications, this is usually unnoticeable (10-50ms added). For latency-sensitive applications like game servers or real-time video, it can matter.

Cloudflare's Terms of Service

Cloudflare's free tier TOS historically restricted serving large amounts of non-HTML content (like video streaming). This has been relaxed significantly, and many people run Jellyfin/Plex through tunnels without issues. However, it's worth being aware that Cloudflare could theoretically enforce bandwidth restrictions. If you're streaming terabytes of video monthly, consider whether a tunnel is the right approach.

WebSocket and long-lived connections

Cloudflare Tunnels support WebSockets, but there are idle timeout limits (usually 100 seconds of inactivity). Applications that rely on persistent WebSocket connections (some real-time apps, VS Code Server) may experience disconnections. You can usually work around this with keep-alive messages.

Single point of failure

Your access depends on Cloudflare's network being available. If Cloudflare has an outage (rare but it happens), all your tunneled services go offline, even for local network users accessing them through the domain. Mitigate this by also configuring local DNS or running a local reverse proxy as a fallback.

Not truly zero-trust

While "zero-trust" is the marketing term, using Cloudflare Tunnels means you're trusting Cloudflare completely. They terminate your TLS, see your unencrypted traffic, and control access to your services. For most self-hosters this is an acceptable trade-off, but it's worth understanding: you're not eliminating trust, you're placing it in Cloudflare rather than your ISP or your own infrastructure.

Common Gotchas

Docker networking

If cloudflared runs in Docker and needs to reach services on the host, localhost inside the container is the container itself, not the host. Options:

# Option 1: Use host networking
services:
  cloudflared:
    network_mode: host

# Option 2: Use host.docker.internal (Docker Desktop) or host gateway
services:
  cloudflared:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    # Then use http://host.docker.internal:8096 in routes

# Option 3: Put services on the same Docker network (recommended)
services:
  cloudflared:
    networks:
      - homelab
  jellyfin:
    networks:
      - homelab
    # Then use http://jellyfin:8096 in routes

Nextcloud and similar apps

Some applications check the incoming host header and reject requests that don't match their configured domain. Add your tunnel domain to Nextcloud's trusted_domains array, or any equivalent configuration in other apps.

Large file uploads

Cloudflare has a 100 MB upload limit on the free plan (300 MB on Pro). If you need to upload large files through a tunneled service (like Nextcloud), this limit will bite you. Workarounds: use the Nextcloud desktop sync client, or upgrade to a Cloudflare paid plan.

This is one of the most common surprises for self-hosters using Cloudflare Tunnels.

Multiple Tunnels vs. One Tunnel

You can route many services through a single tunnel. One cloudflared instance can serve dozens of hostnames. There's no performance reason to create multiple tunnels unless:

For a typical single-server homelab, one tunnel handles everything.

The Honest Trade-offs

Cloudflare Tunnels are great if:

Cloudflare Tunnels are not ideal if:

Bottom line: For most self-hosters who want to access their services remotely, Cloudflare Tunnels are the pragmatic choice. The setup is simple, the free tier is generous, and not having to open ports on your router is a genuine security improvement. Just understand that you're trusting Cloudflare with your traffic, and plan around the upload size limit if it affects your use case.

Resources