“If nobody can even see your ports, how do they hack them?”
The Nightmare Scenario
Picture this: You’ve just set up your shiny new homelab. Excited to access it from anywhere, you forward ports 80, 443, and maybe 22 for good measure. Within minutes, your router logs explode with connection attempts. A Shodan scan reveals your exposed services to the world. Automated bots from Eastern Europe, Asia, and who-knows-where begin their relentless assault. Within hours, a vulnerability in one of your services is exploited, and suddenly your homelab becomes part of a botnet, mining cryptocurrency for strangers.
Now contrast that with a secure homelab: An nmap scan from the internet returns nothing. Port scan? No open ports. Vulnerability scan? There’s nothing to scan. My services are fully accessible to me, yet completely invisible to the world. This is the power of a zero-exposure architecture.
Part 1: Blueprint & Threat Model
The Architecture That Doesn’t Exist (To Attackers)
No Open Ports] CFD[cloudflared] Caddy[Caddy Proxy] subgraph "Trusted Zone" Homepage[Homepage] Mealie[Mealie] OpenWebUI[Open WebUI] AdGuard[AdGuard] end end A -.->|Scan/Attack
BLOCKED| Firewall A -->|HTTPS| CF CF -.->|No Direct Path| Firewall CF <-->|Encrypted Tunnel| CT CT <-->|Outbound Only| CFD WARP <-->|Encrypted| CF CFD -->|Local Only| Caddy Caddy --> Homepage Caddy --> Mealie Caddy --> OpenWebUI Caddy --> AdGuard style A fill:#ff6b6b style Firewall fill:#51cf66 style CFD fill:#339af0 style Caddy fill:#845ef7
Why “Zero-Exposure” Matters
Traditional homelab security follows the castle-and-moat model: strong walls (firewall rules), guards at the gates (authentication), and hope the barbarians don’t find a way in. But what if there were no gates to find?
Zero-exposure architecture flips the script entirely:
- No Attack Surface: Without open ports, there’s literally nothing to attack
- No Discovery: Your services don’t exist on the public internet
- Insider-Only Access: All connections originate from inside your network
- Encrypted Everything: Every byte travels through encrypted tunnels
Part 2: Cloudflare Tunnels + Caddy - The Magic Combination
Turning Inbound Firewall Rules Off
Here’s the beauty of Cloudflare Tunnels - they establish outbound-only connections.
Instead, my cloudflared
daemon reaches out:
Cloudflared Configuration
# /etc/cloudflared/config.yml
tunnel: <tunnel-id>
credentials-file: /etc/cloudflared/<tunnel-id>.json
origincert: /etc/cloudflared/cert.pem
warp-routing:
enabled: true
The tunnel creates a persistent, encrypted connection to Cloudflare’s edge network. All my services are accessible through this tunnel, but the connection originates from inside my network. It’s like having a secret passage that only opens from the inside.
Caddy: The Internal Gatekeeper
While Cloudflare handles the external perimeter, Caddy acts as my internal security guard. Here’s where it gets clever:
Caddy Configuration Example
# Every service gets the same treatment
adguard.themerinowolf.com {
tls {
protocols tls1.2 tls1.3
}
# The key: Local network only!
@local {
remote_ip 192.168.1.0/24 127.0.0.1 ::1
}
handle @local {
reverse_proxy localhost:3000 {
header_up X-Real-IP {remote_host}
}
}
# Everyone else gets rejected
handle {
respond "Access Denied" 403
}
}
This configuration ensures:
- Local-Only Access: Services only respond to requests from the local network
- Automatic HTTPS: Caddy handles all TLS certificates via DNS challenges
- Security Headers: Every response includes security headers
- Access Denial: Non-local requests are explicitly rejected
Automating Certificates Without Exposure
Traditional ACME challenges require port 80 to be open. Not here. Caddy uses DNS challenges via Cloudflare’s API:
DNS Challenge Configuration
{
acme_dns cloudflare {
api_token {$CLOUDFLARE_API_TOKEN}
}
}
This means:
- No HTTP-01 challenges (no port 80)
- No TLS-ALPN-01 challenges (no port 443)
- Just short-lived DNS records that prove domain ownership
The Access Flow
Here’s how a typical request flows through the system:
- Me (Remote): Access
adguard.themerinowolf.com
- Cloudflare DNS: Resolves to Cloudflare’s edge
- Cloudflare Edge: Routes through established tunnel
- cloudflared: Receives request inside my network
- Caddy: Validates request is from tunnel (local IP)
- Caddy: Proxies to adguard UI on port 3000
- Response: Travels back through the same encrypted path
The Bottom Line
Building a zero-exposure homelab isn’t just about security, it’s about peace of mind. While others spend hours analyzing fail2ban logs and implementing increasingly complex firewall rules, I sleep soundly knowing there’s simply nothing for attackers to find.
The combination of Cloudflare Tunnels and Caddy creates an architecture that’s:
- Invisible: No attack surface from the internet
- Accessible: Full functionality for authorized users
- Maintainable: No complex firewall rules to manage
- Scalable: Add new services without touching router configuration
Remember: The most secure port is the one that doesn’t exist. Why expose your kingdom to siege when you can make it invisible instead?
Want to Build Your Own?
The complete code for this setup can be found on GitHub here. Every configuration, every security decision, and every architectural choice has been documented.