Skip to content
Open console

Enter a topic to search documentation.

    Cloudflare Tunnel

    Expose a hub through an outbound tunnel with DNS and TLS configuration.

    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. The cluster must allow outbound tunnel traffic. Keep hub authentication enabled: a tunnel provides connectivity, not application authorization.

    Why Cloudflare Tunnel

    ChallengeCloudflare Tunnel solves it because
    No public IPTunnel connects outbound — no inbound ports needed
    Dynamic IPDNS managed by Cloudflare automatically
    NAT / CGNATOutbound connectivity avoids inbound port forwarding; restrictive egress still needs configuration
    TLS certsFree certs via Let’s Encrypt + DNS validation
    Network exposureNo inbound port forwarding is required for this tunnel; audit other services separately
    TEXT
    Remote Agent → Cloudflare Edge ← Tunnel Pod (your cluster)
                                    Your Hub Service

    Prerequisites

    RequirementNotes
    Cloudflare accountAn account with access to the target zone and Tunnel configuration
    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 token to the target account and the specific DNS zone used for this hub.
    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.

    Terminal
    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:

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

    Create a ClusterIssuer that uses it:

    YAML
    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:

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

    Step 3 — Choose a Cloudflare route integration

    The hub chart supports both a Kubernetes Ingress and a Gateway API HTTPRoute. The commands below use the strrl.dev Ingress controller, so keep ingress.enabled: true in the hub values. If your cluster already runs Faros’s cfgate Cloudflare controller, skip this controller install and use the HTTPRoute values shown below instead; do not enable both integrations for the same hostname.

    Terminal
    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="faros-tunnel"

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

    For the cfgate Gateway API integration, use this route block instead of the ingress block in the next step:

    YAML
    httpRoute:
      enabled: true
      parentRefs:
        - name: cloudflare-tunnel
          namespace: cfgate-system
      hostnames:
        - hub.yourdomain.com
      annotations:
        cfgate.io/origin-protocol: "https"
        cfgate.io/origin-ssl-verify: "false" # the chart's hub origin is self-signed by default

    Step 4 — Deploy the hub

    YAML
    # values-cloudflare.yaml
    hub:
      hubExternalURL: "https://hub.yourdomain.com"
      devMode: false
    
      # Static tokens (list; each is its own tenant user) — or omit for OIDC
      staticAuthTokens:
        - "<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

    Terminal
    helm upgrade --install faros oci://ghcr.io/faroshq/charts/faros-hub \
      -f values-cloudflare.yaml \
      --namespace faros-system \
      --create-namespace

    Step 5 — Verify

    Terminal
    # TLS cert ready?
    kubectl -n faros-system get certificate
    # faros-faros-hub-tls   True   ...
    
    # In the Ingress-controller variant, inspect the generated route.
    kubectl get ingress -n faros-system
    
    # End-to-end
    curl -s https://hub.yourdomain.com/healthz
    # ok
    
    # Log in
    kubectl faros 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:

    YAML
    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

    Terminal
    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

    Terminal
    kubectl -n cert-manager logs -l app=cert-manager
    kubectl -n faros-system describe certificate
    kubectl -n faros-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:

    Terminal
    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:

    Terminal
    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