Cloudflare Access without exposing ports
For years, the standard way to reach a self-hosted app from outside the house was a port forward on the router, maybe with a reverse proxy and a Let’s Encrypt certificate in front of it. It works, but it also means your service is sitting on the public internet, answering anyone who scans for it. Every login page becomes a target for credential stuffing, and every unpatched vulnerability is reachable by the whole world.
These days I don’t open inbound ports for web apps at all. Instead I use Cloudflare Tunnel to carry traffic and Cloudflare Access to decide who is allowed through. Here’s how the pieces fit together and what I’d tell someone setting it up for the first time.
How the model works
A small daemon, cloudflared, runs somewhere inside your network. It makes an outbound connection to Cloudflare’s edge and keeps it open. When someone visits app.example.com, Cloudflare receives the request, checks it against your Access policy, and only then forwards it down the existing tunnel to the internal service.
That has a few nice consequences:
- No inbound firewall rules. Your router doesn’t need any port forwards, and your public IP doesn’t expose the app.
- Identity before the app. Unauthenticated requests never reach your server. The app’s own login page is no longer the first line of defense.
- Per-app policy. Each hostname gets its own rules, so access to the media server says nothing about access to the admin dashboard.
A minimal setup
The broad steps are the same whether you use the dashboard or configuration files:
- Create a tunnel and install
cloudflaredon a host or container that can reach your service. - Add a public hostname to the tunnel that points at the internal service URL.
- In Zero Trust, create an Access application for that hostname.
- Attach a policy, for example “allow these specific email addresses, authenticated through my identity provider with MFA.”
A locally managed tunnel configuration looks something like this:
tunnel: <tunnel-id>
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: git.example.com
service: http://localhost:3000
- hostname: media.example.com
service: http://localhost:8096
- service: http_status:404
That last catch-all rule matters. Without it, a request for an unexpected hostname has nowhere sensible to go. With it, anything you haven’t explicitly published gets a 404.
Policies are the real security boundary
It’s easy to get the tunnel working and then treat the job as done. The tunnel is plumbing; the Access policy is what actually keeps people out. A few habits I’ve settled on:
- Allow lists, not deny lists. Name the people or groups who should get in. Avoid rules like “everyone except…”
- Require MFA at the identity provider. Email one-time PINs are convenient for low-risk apps, but admin tools deserve stronger authentication.
- Shorter sessions for sensitive apps. A dashboard that can change infrastructure doesn’t need a month-long session.
- Review regularly. Policies collect exceptions the same way firewall rules do. Look at them every few months.
Things that tripped me up
Publishing a hostname without an Access app. If you add a public hostname to a tunnel but forget to create a matching Access application, the service is reachable by anyone. I now create the Access application first, then publish the hostname.
Non-browser clients. Git over HTTPS, mobile apps and API clients can’t complete an interactive login. For those, use service tokens with a narrowly scoped policy, or keep that traffic on a VPN instead.
Assuming the app is now safe. Access reduces exposure dramatically, but it isn’t a substitute for patching. Anyone who passes the policy still talks to the same software.
Where a VPN still fits
Tunnel and Access are great for web applications. For full network access, such as SSH to many hosts, file shares or troubleshooting, I still keep a WireGuard VPN available. The two complement each other: Access for day-to-day apps, VPN for the occasional deep work.
Bottom line
If you’re still forwarding ports to reach self-hosted web apps, try moving one service behind a tunnel and an identity policy. Your router’s attack surface shrinks to nothing, every request is tied to an identity, and you get a clean audit trail of who accessed what. It’s one of the best security improvements you can make to a home lab in an afternoon.
Questions or corrections? Email me.
