Deploy your own hub

Cloudflare Tunnel

Expose your hub via Cloudflare Tunnel — no public IP, free TLS, free DDoS protection.

Cloudflare Tunnel opens an outbound connection from your cluster to Cloudflare’s edge. Cloudflare then routes inbound traffic to your hub through that tunnel — no inbound firewall rules, no public IP, no port forwards. It’s the smoothest way to put a home-lab hub on the internet.

Why Cloudflare Tunnel

ChallengeCloudflare Tunnel solves it because
No public IPTunnel connects outbound — no inbound ports needed
Dynamic IPDNS managed by Cloudflare automatically
NAT / CGNATWorks through any NAT
TLS certsFree certs via Let’s Encrypt + DNS validation
DDoSBuilt into Cloudflare’s edge
SecurityNo exposed ports on your network
Remote Agent → Cloudflare Edge ← Tunnel Pod (your cluster)
                                Your Hub Service

Prerequisites

RequirementNotes
Cloudflare accountFree tier is fine
Domain on CloudflareDNS for the domain must be managed by Cloudflare
API tokenWith Cloudflare Tunnel: Edit and DNS: Edit
Account IDFound in the dashboard sidebar

Create a Cloudflare API token

  1. Cloudflare DashboardMy ProfileAPI TokensCreate TokenCustom Token.
  2. Permissions:
    • AccountCloudflare TunnelEdit
    • ZoneDNSEdit
  3. Restrict the zone to your domain (or “All zones” if you don’t care).
  4. Continue to summaryCreate Token. Copy the token; you can’t see it again.

Find your account ID

Any domain page in the dashboard → right sidebar → APIAccount ID.

Step 1 — Install cert-manager

cert-manager issues TLS certificates for the hub. We use DNS-01 validation via Cloudflare so cert issuance works even before the hub is publicly reachable.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.0/cert-manager.yaml
kubectl -n cert-manager wait --for=condition=ready pod \
  -l app.kubernetes.io/instance=cert-manager --timeout=120s

Step 2 — Configure DNS-01 validation

Store the API token as a secret in the cert-manager namespace:

kubectl create secret generic cloudflare-api-token \
  --namespace cert-manager \
  --from-literal=api-token="YOUR_CLOUDFLARE_API_TOKEN"

Create a ClusterIssuer that uses it:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token

Verify:

kubectl get clusterissuer letsencrypt-prod
# letsencrypt-prod   True    30s

Step 3 — Install the Cloudflare Tunnel ingress controller

helm repo add strrl.dev https://helm.strrl.dev
helm repo update

helm upgrade --install --wait \
  -n cloudflare-tunnel-ingress-controller --create-namespace \
  cloudflare-tunnel-ingress-controller \
  strrl.dev/cloudflare-tunnel-ingress-controller \
  --set=cloudflare.apiToken="YOUR_CLOUDFLARE_API_TOKEN" \
  --set=cloudflare.accountId="YOUR_CLOUDFLARE_ACCOUNT_ID" \
  --set=cloudflare.tunnelName="kedge-tunnel"

In the Cloudflare Zero Trust dashboardNetworks → Tunnels you should see kedge-tunnel as Healthy.

Step 4 — Deploy the hub

# values-cloudflare.yaml
hub:
  hubExternalURL: "https://hub.yourdomain.com"
  devMode: false

  # Pick one — static token here, or omit for OIDC
  staticAuthToken: "<openssl rand -hex 32>"

  tls:
    selfSigned:
      enabled: false
    certManager:
      enabled: true
      issuerRef:
        name: letsencrypt-prod
        kind: ClusterIssuer
      dnsNames:
        - "hub.yourdomain.com"

ingress:
  enabled: true
  className: "cloudflare-tunnel"
  hosts:
    - host: hub.yourdomain.com
      paths:
        - path: /
          pathType: ImplementationSpecific
helm upgrade --install kedge oci://ghcr.io/faroshq/charts/kedge-hub \
  -f values-cloudflare.yaml \
  --namespace kedge-system \
  --create-namespace

Step 5 — Verify

# TLS cert ready?
kubectl -n kedge-system get certificate
# kedge-kedge-hub-tls   True   ...

# Ingress address (will be xxxx.cfargotunnel.com)
kubectl get ingress -n kedge-system

# End-to-end
curl -s https://hub.yourdomain.com/healthz
# ok

# Log in
kubectl kedge login --hub-url https://hub.yourdomain.com

Exposing additional services through the same tunnel

The tunnel can carry multiple hostnames. If you’re running Dex for OIDC, give it the same ingressClassName:

ingress:
  enabled: true
  className: "cloudflare-tunnel"
  hosts:
    - host: idp.yourdomain.com
      paths:
        - path: /
          pathType: ImplementationSpecific

The controller creates the CNAME automatically.

Troubleshooting

Tunnel not connecting

kubectl -n cloudflare-tunnel-ingress-controller logs \
  -l app.kubernetes.io/name=cloudflare-tunnel-ingress-controller

Common causes:

  • Invalid API token — Re-create with Cloudflare Tunnel: Edit + DNS: Edit
  • Wrong account ID — Double-check in the dashboard
  • Tunnel name conflict — Delete stale tunnels from the Cloudflare dashboard

Certificate not issuing

kubectl -n cert-manager logs -l app=cert-manager
kubectl -n kedge-system describe certificate
kubectl -n kedge-system get certificaterequest,order,challenge

Most failures here are missing DNS:Edit on the API token, or the token doesn’t cover the zone.

DNS not resolving

The ingress controller creates a CNAME pointing at xxxx.cfargotunnel.com. Check it propagated:

dig hub.yourdomain.com CNAME

kind on macOS — slow DNS-01 challenges

If cert-manager keeps re-trying the DNS-01 challenge on a kind cluster, force it to use public resolvers:

kubectl -n cert-manager patch deployment cert-manager --type=json -p='[
  {"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53"},
  {"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers-only"}
]'

References