Asia VPNAsiaVPNGet app
All posts

Cloudflare Tunnel: Reach Home Services Without Port Forwarding

The PiVPN guide opens with a check that stops a lot of people: if your ISP puts you behind CGNAT, you cannot forward a port, and a home VPN server has nothing to listen on. That post says "you are stuck". This one is the way around it.

A Cloudflare Tunnel makes your home service reachable without forwarding anything, without a static IP, and without opening a single inbound port. It is genuinely a better answer than port forwarding for most self-hosted services.

It is also a different trust arrangement than a VPN, in a way that matters and that most tutorials skip. That section is at the end and it is the reason to read this rather than the quickstart.

Why port forwarding fails

Three separate problems, and a tunnel solves all three:

CGNAT. Your router's WAN address is in 100.64.0.0/10 - a shared address your ISP hands to thousands of subscribers. There is no port to forward because the public IP is not yours. Common on mobile broadband and increasingly on fibre in Vietnam and across Southeast Asia.

Dynamic IP. Even with a real public address, it changes. Dynamic DNS patches over this but adds a propagation delay every time.

An open port is an open port. Forwarding 443 to a Raspberry Pi means every scanner on the internet can reach whatever is listening, and the security of your home network is now the security of that one service's authentication.

What a tunnel actually is

cloudflared is a daemon that runs next to your service and dials out:

"cloudflared initiates an outbound connection through your firewall from the origin to the Cloudflare global network."

Once that connection is up, traffic flows both ways over it. Visitors reach Cloudflare's edge; Cloudflare hands the request down the tunnel your daemon already opened. Nothing connects inbound to your house.

That inverts the firewall problem. Outbound connections are what firewalls allow by default and what CGNAT was designed to pass. Cloudflare's own documentation is direct about the consequence:

"You can then configure your firewall to allow only these outbound connections and block all inbound traffic."

Your origin IP stops being public. It is not obscured - it is genuinely never handed out.

Two ways to manage a tunnel

Before you start, pick one. They are not interchangeable later without redoing work.

Remotely-managed stores the tunnel's configuration on Cloudflare and you edit it in the dashboard, API, or Terraform. Locally-managed keeps configuration in a config.yml on the machine and you manage it from the CLI.

Cloudflare recommends remotely-managed for most use cases, and for a home service that recommendation is right: the box at home is the thing most likely to be reinstalled, and keeping its config off it means a rebuild is one cloudflared install away.

Use locally-managed when you want the config in version control alongside everything else, or for local development.

Setting one up

The dashboard path is: Zero Trust → Networks → Tunnels → Create a tunnel, name it, and Cloudflare gives you a one-line install command containing the tunnel token. Run it on the machine. Then add a public hostname mapping home.example.com to http://localhost:8096 (or whatever your service is).

That is the whole thing. DNS, certificate and routing are handled.

The CLI path, if you prefer locally-managed:

cloudflared tunnel login
cloudflared tunnel create home
# ~/.cloudflared/config.yml
tunnel: <TUNNEL-UUID>
credentials-file: /root/.cloudflared/<TUNNEL-UUID>.json

ingress:
  - hostname: jellyfin.example.com
    service: http://localhost:8096
  - hostname: grafana.example.com
    service: http://localhost:3000
  - service: http_status:404
cloudflared tunnel route dns home jellyfin.example.com
cloudflared tunnel run home

The final catch-all service: http_status:404 rule is required - the ingress list must end with a rule that matches everything, or cloudflared refuses to start.

Once it works, install it as a service so it survives a reboot:

sudo cloudflared service install
sudo systemctl enable --now cloudflared

The domain has to be on a Cloudflare zone in your account. That is the one prerequisite there is no way around.

Put authentication in front of it

A tunnel makes a service reachable. It does not make it private - without another step, jellyfin.example.com is now a public URL and its own login page is your only defence.

Cloudflare Access fixes that at the edge, before requests reach your house. Create an application for the hostname, add a policy - allow a specific email, a Google or GitHub identity, an email domain - and unauthenticated requests are rejected by Cloudflare rather than by your Pi.

Zero Trust is free for up to 50 users, which for a household is effectively unlimited. Past that it is $3/user for standalone Access. For anything you would not want indexed, this step is not optional.

What it costs

The tunnel itself is free, on any Cloudflare plan, with no bandwidth charge for the tunnel connection.

Two caveats worth stating rather than discovering:

Traffic through the tunnel is served through Cloudflare's CDN, so the CDN service terms apply - the ones covering video and large files hosted outside Cloudflare, described in the CloudFront and Cloudflare comparison. Putting a home media server behind a tunnel and streaming 4K through it is the case those terms exist to address. Enforcement is a suspension, not a bill.

And the free tier's limits are on Zero Trust seats, not on tunnels. You can run as many tunnels as you like.

The trust trade you are making

This is the part to be honest about, because it is the real difference between a tunnel and a VPN, and it is not a footnote.

Cloudflare terminates TLS. The certificate visitors validate is Cloudflare's. Traffic is decrypted at the edge, inspected and routed, then re-encrypted down the tunnel to you. Cloudflare can see the plaintext of everything passing through. That is not a flaw - it is how a reverse proxy works, and it is what makes Access policies and DDoS filtering possible.

Compare that to WireGuard on a Pi at home: the keys are yours, nothing in the middle can read the traffic, and no third party knows what services exist.

So the two tools answer different questions:

Cloudflare TunnelHome VPN (WireGuard)
Works behind CGNATYesNo
Inbound ports neededNoneOne
Who can read the trafficCloudflare, at the edgeOnly the endpoints
Sharing with non-technical peopleA URL and a loginInstall an app, import a profile
Reaching arbitrary devices on the LANNot by defaultYes
Exposing a service publiclyThe point of itNot what it does

Use a tunnel when you want a specific service reachable - from any device, including ones you cannot install a VPN client on, by people you do not want to hand a .conf file to.

Use a VPN when you want your own devices on your own network, when the traffic must not be readable by anyone in the middle, or when you need to reach the whole LAN rather than one HTTP service.

Running both is normal and is usually the right answer: a tunnel for the things other people use, WireGuard for you.

Things that will trip you up

It is HTTP-shaped by default. Public hostname routing covers HTTP and WebSockets well. Raw TCP and SSH work, but through cloudflared access on the client side or via WARP - not by pointing a browser at a hostname.

The catch-all ingress rule. Covered above, and the most common reason cloudflared will not start.

A tunnel and a VPN on the same box need thinking about. If the Pi routes all its traffic through a VPN, cloudflared may dial out through that tunnel too and you have built something you will not enjoy debugging.

You now depend on Cloudflare for reachability. No tunnel, no access - even from inside your own house, unless you keep a local hostname pointing at the LAN address. Worth setting up before you need it.


Cover photo by User_Pascal on Unsplash.

Keep reading