Talos OS: Exposing a Homelab Service with Cloudflare Tunnel
Introduction
Between the cluster, the
storage,
and Postgres,
I’ve got a real place to run apps now. All of that is still only reachable
from inside my LAN, though. This post is getting a service out to the
internet without opening a port on my home router or publishing my home IP
anywhere - cloudflared running as an ingress controller inside the
cluster.
Why a Tunnel Instead of Port Forwarding
cloudflared makes an outbound connection from inside the cluster out to
Cloudflare’s edge and keeps it open. Traffic comes in over that connection,
not through an inbound port on my router. There’s nothing to forward, no
firewall rule pointing at my home IP, and no home IP exposed in DNS at all - every request goes through Cloudflare’s edge first.
Routing everything through that edge also means bot protection, WAF rules, and rate limiting are available on the same traffic, configured in Cloudflare rather than in the app or on the tunnel itself - nginx here doesn’t have to know any of that exists. I haven’t turned any of it on yet for this dummy app, but it’s sitting there for when something behind this tunnel actually needs it.
Prerequisites
- The Talos cluster from the
first post and the
ceph-rbdstorage class from the second post. - A domain onboarded to a Cloudflare account.
- Helm 3.
- A Cloudflare API token scoped to manage Tunnels.
- The
cloudflare-tunnel-ingress-controllerHelm chart.
Installing the Tunnel Ingress Controller
I run the cloudflare-tunnel-ingress-controller chart, which both manages
the cloudflared pods and watches Kubernetes Ingress objects to create
the matching tunnel routes and DNS records automatically:
cloudflare-tunnel-ingress-controller:
cloudflare:
accountId: "<account-id>"
tunnelName: "homelab-ingress"
apiToken: "<api-token>"
cloudflared:
replicaCount: 2
podAntiAffinity: true
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
pdb:
enabled: true
minAvailable: 1❯ helm install cloudflare-ingress cloudflare/cloudflare-tunnel-ingress-controller \
-n cloudflare-ingress --create-namespace -f values.yamlI didn’t start with the replicaCount, podAntiAffinity, and resources
block above - the chart defaults to a single replica with no resource
limits at all, which meant every app behind the tunnel went down with that
one pod. I also caught it stuck on a stale image build: cloudflared warns
“outdated” on every startup, and left on latest with IfNotPresent it
just never re-pulls once a pod already exists. Pinning the tag and running
two replicas spread across nodes with a PodDisruptionBudget fixed both.
Pointing an App at It
Any Ingress with ingressClassName: cloudflare-tunnel gets picked up by
the controller automatically - no manual cloudflared tunnel route dns
step. To prove it works, a dummy nginx deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
spec:
replicas: 1
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-demo
spec:
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-demo
spec:
ingressClassName: cloudflare-tunnel
rules:
- host: nginx.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-demo
port:
number: 80❯ kubectl apply -f nginx-demo.yaml
❯ curl -sI https://nginx.example.com
HTTP/2 200
server: cloudflare
content-type: text/htmlThat request went from my laptop, out to Cloudflare, back down through the tunnel, into the cluster, and hit nginx - without touching my router at all:
Conclusion
The app is reachable from the internet now, which is the point, but also the problem: right now anyone with the URL can hit it, nginx welcome page and all. Next post: putting Cloudflare Zero Trust in front of this so it actually requires a login.