The Problem
Exposing homelab services to the internet is a solved problem with multiple approaches, but each carries a different risk profile. The traditional answer is a VPN -- WireGuard, OpenVPN, or Tailscale -- and it works. But VPN access is binary: you're either on the network or you're not. That model breaks down when you need granular, per-service access for different audiences.
I run services that serve different trust levels. Some are personal tools that only I should ever touch. Others are shared with a small group. A few are fully public. A VPN-only approach forces everything through a single gate, and the access control logic ends up scattered across individual services rather than centralized.
What Cloudflare Tunnels Changed
Cloudflare Tunnels flip the networking model. Instead of opening inbound ports on a firewall, the tunnel agent (cloudflared) makes an outbound connection to Cloudflare's edge. Traffic routes through Cloudflare's network and arrives at your origin through that established tunnel. No public IP exposure. No port forwarding. No NAT traversal headaches.
The key insight is that the tunnel itself is not the access control layer -- it is the transport layer. Access control is handled separately through Cloudflare Access policies or, in my case, through Authentik sitting behind Traefik.
Architecture
The flow looks like this:
User Request
→ Cloudflare Edge (TLS termination, DDoS protection, WAF)
→ Cloudflare Tunnel (encrypted connection to origin)
→ Traefik (routing, middleware, additional TLS)
→ Authentik (identity verification, SSO)
→ Target Service
Each layer handles a specific concern. Cloudflare filters malicious traffic before it ever reaches my infrastructure. Traefik routes requests to the correct backend based on hostname. Authentik enforces authentication and authorization policies per application.
Why Not Just WireGuard
WireGuard is excellent. I still use it for administrative access to the hypervisor layer and for services that should never be reachable from the public internet. The distinction is about audience and access pattern:
- WireGuard: Admin-only access. SSH to Proxmox hosts, direct database connections, management interfaces. The audience is me, from known devices.
- Cloudflare Tunnels: Service-level access. Web applications, dashboards, APIs that might need to be reached from varying locations or shared with specific people.
This is not a replacement -- it is a complementary layer. WireGuard handles the "trusted operator" path. Cloudflare Tunnels handle the "authenticated user" path.
Trade-offs I Accepted
Nothing is free. Cloudflare Tunnels introduce dependencies:
- Cloudflare as a dependency -- If Cloudflare goes down, external access goes down. I mitigate this with WireGuard as a fallback for critical admin paths.
- TLS inspection -- Cloudflare terminates TLS at their edge. They can, in theory, inspect traffic. For my homelab use case, this is acceptable. For anything requiring true end-to-end encryption, I use a different approach.
- Vendor lock-in -- The tunnel agent is Cloudflare-specific. Moving to a different provider means rearchitecting the ingress layer. I accept this because the alternative (managing public IP exposure) carries higher operational risk.
- DNS dependency -- All tunneled services route through Cloudflare DNS. This is fine since I already use Cloudflare as my DNS provider.
The Result
Six months in, the operational burden of external access dropped significantly. No firewall rules to audit for port forwarding. No dynamic DNS updates. No NAT traversal debugging. The attack surface of the network perimeter is minimal because there is no exposed perimeter.
New services get external access by adding a tunnel route and an Authentik application entry. Decommissioning is equally simple. The entire lifecycle is managed through configuration, not infrastructure changes.
What I Would Do Differently
If I were starting fresh, I would set up the Cloudflare Tunnel before any other external access method. I spent time with reverse proxy configurations and port forwarding that ended up being replaced entirely. The tunnel-first approach simplifies everything downstream.
I would also invest earlier in Authentik policy templates. Each new application required writing access policies from scratch initially. Templating common patterns (internal-only, shared-with-group, public-with-auth) would have saved iteration time.