portfolio Anshul Bisen
ask my work

Setting up Cloudflare Tunnels so I never open a port on my homelab again

I replaced every port forward with a zero-trust Cloudflare Tunnel in a single evening. Here is the exact setup.

My homelab firewall looked like Swiss cheese. Port 80 forwarded to Caddy. Port 443 also forwarded to Caddy. Port 3000 forwarded to Grafana. Port 8080 forwarded to a development server. Port 5432 forwarded to PostgreSQL because I wanted remote access from my laptop at a coffee shop. Every one of those port forwards was a hole in my network that any port scanner on the internet could find in minutes.

I knew this was bad. I told myself I would set up a VPN “soon.” Then I discovered Cloudflare Tunnels and realized I could do something better: expose every service I needed without opening a single port on my router. The entire migration took one evening.

The kind of infrastructure that teaches you by breaking.

A lot of my month-one leadership came through infrastructure choices that looked small from the outside. It also builds on what I learned earlier in “The AWS bill that made me rethink everything about infrastructure.” I was building the muscle memory that later fed the infrastructure and ctrlpane projects at home: reproducible defaults, cheap feedback loops, and enough observability that I did not need to guess under pressure.

The infrastructure mess that made the lesson stick.

How Cloudflare Tunnels Work

The concept is elegant. Instead of opening inbound ports on your network, a lightweight daemon called cloudflared runs on your server and establishes an outbound connection to Cloudflare’s edge. Traffic from the internet hits Cloudflare first, gets authenticated and filtered, and then flows through the existing outbound connection to your service. Your router never needs an inbound rule.

  • Your server connects outbound to Cloudflare. No inbound ports needed.
  • Cloudflare terminates TLS at the edge with your domain’s certificate.
  • Each service gets a DNS record like grafana.yourdomain.com that routes through the tunnel.
  • Access policies can require authentication before the request even reaches your server.
  • All traffic is encrypted end-to-end through the tunnel.

The security improvement is night and day. Instead of twelve services directly exposed to the internet, I have zero open ports and all traffic flows through Cloudflare’s DDoS protection, WAF, and access control.

The Setup

I run cloudflared as a Kubernetes DaemonSet on my k3s cluster so it starts automatically and restarts if it crashes. The tunnel configuration maps DNS hostnames to local services.

# cloudflared config.yml
tunnel: homelab-main
credentials-file: /etc/cloudflared/credentials.json
ingress:
- hostname: grafana.sdfsdf.in
service: http://grafana.monitoring:3000
- hostname: git.sdfsdf.in
service: http://gitea.default:3000
- hostname: wiki.sdfsdf.in
service: http://wiki.default:3000
- hostname: api.sdfsdf.in
service: http://api-gateway.default:8080
# Catch-all rule (required)
- service: http_status:404

Each hostname maps to a Kubernetes service endpoint. Cloudflare handles TLS termination and DNS. The services inside the cluster only need to listen on HTTP. No certificate management, no Let’s Encrypt renewal scripts, no Caddy or Nginx configuration for TLS.

DNS Automation

Adding a new service used to mean: create the service, configure the reverse proxy, add a DNS record, wait for propagation, and maybe remember to get a TLS certificate. With Cloudflare Tunnels, adding a service is two steps: add an ingress rule to the config and restart cloudflared. But I automated even that.

add-tunnel-service.sh
#!/bin/bash
# Usage: ./add-tunnel-service.sh myapp myapp.default:8080
HOSTNAME="$1.sdfsdf.in"
SERVICE="http://$2"
# Add ingress rule to config
yq -i ".ingress = [{\"hostname\": \"$HOSTNAME\", \"service\": \"$SERVICE\"}] + .ingress" \
/etc/cloudflared/config.yml
# Create DNS record via Cloudflare API
curl -X POST \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"$1\",\"content\":\"$TUNNEL_ID.cfargotunnel.com\",\"proxied\":true}"
# Restart cloudflared
kubectl rollout restart daemonset/cloudflared

One command, one argument, and a new service is publicly accessible with TLS in about ninety seconds. The script handles the config update, DNS record creation, and daemon restart. I have used it to add eight services since the initial setup.

Access Policies

Not every service should be publicly accessible. Grafana dashboards, database admin panels, and development servers need authentication before anyone can reach them. Cloudflare Access lets me add authentication policies per hostname without touching the application.

  • Grafana: Requires login with my Google account via Cloudflare Access. Anyone hitting grafana.sdfsdf.in without authentication gets a Cloudflare login page.
  • Git server: Same Google authentication policy. Public repositories are readable, but push access requires tunnel authentication plus Git SSH keys.
  • API endpoints: No Cloudflare Access policy. These are public APIs that handle their own authentication via API keys and JWT tokens.
  • Development servers: Access restricted to my specific IP range via Cloudflare Access policies. This is the coffee shop use case that previously required PostgreSQL port forwarding.

The critical win is that authentication happens at Cloudflare’s edge before traffic reaches my network. An unauthorized request never touches my homelab. It gets rejected 200 milliseconds away at the nearest Cloudflare data center.

The Result

After the migration, my router’s port forwarding table is empty. Zero inbound rules. The attack surface of my home network is the same as someone who is not running a homelab at all. Every service is accessible via a clean hostname with automatic TLS, and the sensitive ones require authentication before the request crosses the internet to my closet.

The total cost of this setup is zero dollars. Cloudflare Tunnels is included in the free plan. Cloudflare Access is free for up to 50 users. For a homelab operator, this is the best security improvement per dollar you can make.

Homelab, but treated like a real environment.

Looking back, this is one of those builder-phase decisions that bought me leadership credibility before I had any leadership title equity. I was still proving I could be trusted with the boring, consequential calls. That instinct carried straight into infrastructure and ctrlpane.

If you are running a homelab with port forwards, stop reading and go set up Cloudflare Tunnels. It took me one evening and eliminated every open port on my network. There is no reason to expose your home IP to the internet anymore.