You’ve got a home server. Maybe Proxmox, a NAS, a self-hosted app stack, or just a Raspberry Pi running some containers. And you want remote access without touching your firewall.
The instinctive move is to open ports. Don’t. Your home router’s public IP is being scanned continuously. One misconfigured service — one unauthenticated endpoint, one unpatched daemon — and you’ve handed over your local network to someone who will do something interesting with it.
The opposite instinct is to run a VPN server on the same exposed machine. Same problem, narrower window: now you’re managing key rotation, client configs, and patches on a daemon that’s listening on a public UDP port.
There’s a better answer. This post sets up a full Cloudflare Zero Trust access layer for a home lab starting from nothing: outbound-only tunnels, identity-aware access policies, DNS-level protection, and device posture checks. No ports opened on your router. No static IP required. Free tier.
I’ve covered the combined Cloudflare + Tailscale picture before. This post goes deeper on the Cloudflare-only path — when you want to stay in one stack and get serious about policy and posture.
Why Cloudflare’s Network Matters
Before the setup steps: the reason this works is Cloudflare’s global network, and it’s worth understanding what you’re actually plugging into.
Cloudflare runs 300+ points of presence globally, connected by their own private fiber backbone. WARP traffic doesn’t leave to the public internet until it exits the nearest Cloudflare PoP to your destination. Cloudflare Tunnel connections are Anycast — your cloudflared daemon connects to Cloudflare’s edge IPs, and BGP routes that connection to the physically closest PoP automatically.
What this means in practice:
- Your home IP is never exposed. The tunnel is outbound-only from your server. Cloudflare presents its own edge IPs to the world. No one scanning the internet can find your machine.
- DDoS protection is on by default. Cloudflare sits in the path for every request, which means their L3/L4 mitigation applies to your home lab services without any configuration.
- Latency is competitive with sitting on your local network, once the PoP hop is near both your device and your home connection. For most WARP use cases it’s indistinguishable.
This isn’t theoretical. It’s the same network that absorbs some of the largest DDoS attacks recorded, and you’re using it as your home lab’s edge.
The Stack
Four Cloudflare products, all free tier, all working together:
| Product | What it does |
|---|---|
| Cloudflare Tunnel | Outbound-only connector on your server. Creates a persistent encrypted tunnel to CF’s edge. No inbound ports. |
| Cloudflare Access | Identity-aware proxy in front of each service. Evaluates who you are and whether your device is compliant before a request gets through. |
| Cloudflare Gateway | DNS resolver for enrolled devices. Blocks malware and phishing domains at query time, before a connection is established. |
| WARP | Device-side agent. Enrolls the device into your Zero Trust org, routes DNS through Gateway, enables posture checks. |
The free tier covers 50 users and unlimited applications. For a home lab, that’s effectively unlimited.
How Traffic Flows
Here’s what actually happens when you access a home service from a remote device:
sequenceDiagram
participant D as Device (WARP enrolled)
participant GW as Cloudflare Gateway
participant E as Cloudflare Edge
participant AC as Cloudflare Access
participant T as cloudflared (home server)
participant S as Service (e.g. Proxmox)
D->>GW: DNS query → proxmox.yourdomain.com
GW-->>D: Resolves (DNS policy applied)
D->>E: HTTPS request (via WARP tunnel)
E->>AC: Evaluate access policy
AC-->>E: Identity + posture check passes
E->>T: Request forwarded through tunnel
T-->>S: Proxied to local service
S-->>D: Response (reverse path)
The key property: no inbound connection to your home network at any step. The cloudflared daemon initiates an outbound connection to Cloudflare’s edge at startup and holds it open. When a request arrives at the edge, it’s forwarded down that already-established tunnel. Your router firewall can block all inbound traffic and this still works.
Wiring It Up
1. Create a Cloudflare Zero Trust organization
Go to one.dash.cloudflare.com and create a free account. Pick a team name (e.g. hicke) — this becomes part of your WARP enrollment URL.
2. Install and configure the tunnel
On your home server (Linux, Raspberry Pi, Docker — anything):
# Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
-o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
# Authenticate (opens browser for one-time login)
cloudflared tunnel login
# Create the tunnel
cloudflared tunnel create homelab
This creates a credential file at ~/.cloudflared/<tunnel-id>.json. Keep it on the server, don’t share it — it’s what proves to Cloudflare’s edge that your tunnel is yours.
Configure ingress in ~/.cloudflared/config.yml:
tunnel: <tunnel-id>
credentials-file: /root/.cloudflared/<tunnel-id>.json
ingress:
- hostname: proxmox.yourdomain.com
service: https://localhost:8006
originRequest:
noTLSVerify: true # Proxmox uses self-signed cert locally
- hostname: vault.yourdomain.com
service: http://localhost:8200
- hostname: nas.yourdomain.com
service: http://localhost:5000
- service: http_status:404 # catch-all — required
Run it as a system service:
cloudflared service install
systemctl enable --now cloudflared
Add DNS records: for each hostname, create a CNAME in Cloudflare DNS pointing to <tunnel-id>.cfargotunnel.com. The cloudflared tunnel route dns command can do this automatically:
cloudflared tunnel route dns homelab proxmox.yourdomain.com
3. Protect services with Access policies
In the Zero Trust dashboard → Access → Applications, add an application for each service. Set the application type to Self-hosted, point it at proxmox.yourdomain.com, and define a policy.
The simplest policy — everyone at your email domain:
Rule: Emails ending in @youremail.com
More precise:
Rule: Email is henric@youremail.com
With GitHub (useful for shared lab access):
Rule: GitHub organization is your-org
When Cloudflare Access is in front of a service, every HTTP request hits a Cloudflare-issued identity check first. Fail the check, get a 403. The backend service sees only requests that already passed — and Cloudflare injects a signed JWT into the request headers that your app can verify if it wants to double-check identity.
For SSH and RDP specifically, Access has a browser-rendered terminal mode — no client software needed, just a browser and an identity check:
# Or native SSH without a public port, using the Access SSH helper
cloudflared access ssh --hostname ssh.yourdomain.com
4. Enroll devices with WARP
Install WARP on your laptop or phone from one.one.one.one. In the WARP settings → Account → Login with Cloudflare Zero Trust, enter your team name. The device enrolls and you can now require device enrollment as a condition on Access policies.
Once enrolled, all DNS queries from the device route through Cloudflare Gateway automatically. That’s DNS-level malware and phishing blocking, no additional config.
Zero Trust Network Access: Policies and Posture
This is where the setup goes from “convenient remote access” to actual Zero Trust.
Access policies
Every Access application can require conditions beyond identity. You can layer them:
Policy: Allow
Condition: Email is henric@youremail.com
Condition: WARP client is enrolled
Condition: Operating system is not Windows XP
Or enforce team-wide:
Policy: Allow
Condition: GitHub organization is your-org
Condition: Device posture check: disk encryption enabled
If any condition fails, access is denied — the request never reaches your server.
Device posture checks
In Settings → WARP Client → Device posture, you configure checks that the WARP client evaluates on the device and reports to the edge:
| Check | What it verifies |
|---|---|
| OS version minimum | Device is on a supported OS version |
| Disk encryption | FileVault/BitLocker/LUKS enabled |
| Application check | A specific process is running (e.g. your EDR agent) |
| File check | A specific file exists (useful for managed device certificates) |
| Domain joined | Device is enrolled in your directory |
These posture signals feed into your Access policies. A device that fails the disk encryption check gets denied access to the Proxmox admin panel, even if the user authenticated successfully. This is the difference between Zero Trust and a traditional VPN: the VPN says “you authenticated, here’s the network” — ZTNA says “you authenticated and your device looks right for this specific service, here’s that service.”
Gateway DNS policies
Under Gateway → DNS Policies, you can filter by category (malware, phishing, adult content), specific domains, or custom rules. These apply to all WARP-enrolled devices the moment they’re configured — no agent config changes, no client deployments.
Block all known malware domains for every enrolled device:
Action: Block
Category: Malware
Block a specific domain:
Action: Block
Domain: badsite.example.com
Allow-list your own services and block everything that doesn’t need to be public from a managed device:
Action: Block
Category: Newly registered domains
DNS is where most malware C2 traffic lives. Blocking at query time is the cheapest possible layer to enforce — no packet inspection, no TLS interception, just: “did you ask for a bad domain? No. Then here’s the IP.”
What You Get vs. a Traditional VPN
| Traditional VPN | Cloudflare Zero Trust | |
|---|---|---|
| Open ports on router | Yes (UDP 1194/51820) | No |
| Per-service access control | No — full network access | Yes — per-application policies |
| Device posture enforcement | No | Yes |
| DNS-level protection | No | Yes (Gateway) |
| Identity provider integration | Rare / expensive | Google, GitHub, Okta, etc. |
| DDoS protection | None | Cloudflare network |
| Home IP exposed | Yes | No |
| Maintenance | Key rotation, patches | Cloudflared binary updates |
The VPN wins on one dimension: raw performance for bulk transfers within your home network. WARP adds a round-trip to Cloudflare’s edge. For SSH sessions, web UIs, and API calls, it’s imperceptible. For Proxmox console access or large NAS file transfers, you’ll notice it — and for those you can always fall back to Tailscale for direct LAN-speed mesh access alongside this stack.
Summary
The full setup takes about an hour. What you end up with:
- No open firewall ports — ever. The tunnel is outbound-only from your server.
- Identity-aware access — every service gated behind a Cloudflare Access policy. One compromised credential doesn’t expose your whole lab.
- Device posture enforcement — disk encryption, OS version, enrollment status checked on every access request.
- DNS-level protection — malware and phishing blocked at query time for every enrolled device.
- Cloudflare’s global edge — your home IP is invisible; DDoS protection is on by default; PoP proximity keeps latency low.
This is what secrets management architecture looks like applied to network access: stop trusting the network, start trusting the identity. The VPN model grants access to a network and hopes nothing goes wrong. This model grants access to exactly what was asked for, after verifying exactly who is asking and from what device.