Skip to content
Railgrid Docs
Sections
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 Dashboard → My Profile → API Tokens → Create Token → Custom Token.
    2. Permissions:
      • Account → Cloudflare Tunnel → Edit
      • Zone → DNS → Edit
    3. Restrict the token to the target account and the specific DNS zone used for this hub.
    4. Continue to summary → Create Token. Copy the token; you can’t see it again.

    Find your account ID

    Any domain page in the dashboard → right sidebar → API → Account 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: your-email@example.com
        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 Railgrid’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="railgrid-tunnel"

    In the Cloudflare Zero Trust dashboard → Networks → Tunnels you should see railgrid-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>"
    
      # The pod CIDR cloudflared runs in. The hub sees cloudflared as its peer and
      # only trusts X-Forwarded-For from these ranges; without this every visitor
      # shares one sign-in rate-limit bucket.
      trustedProxyCIDRs:
        - 10.42.0.0/16
    
      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 railgrid oci://ghcr.io/railgrid/charts/railgrid-hub \
      -f values-cloudflare.yaml \
      --namespace railgrid-system \
      --create-namespace

    Step 5 — Verify

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

    Keep every hostname one label below the apex

    Cloudflare’s free Universal SSL covers exactly one level: *.yourdomain.com and yourdomain.com. hub.yourdomain.com and myapp-ab12cd.apps-zone.com are covered; hub.dev.yourdomain.com is not, and fails the TLS handshake before the request reaches the tunnel because no certificate matches the SNI. Name environments with a hyphen (hub-dev.yourdomain.com) rather than a sub-zone, and serve published applications from a zone apex (<app>-<hash>.apps-zone.com) rather than *.apps.yourdomain.com.

    Verify the exact hostname; SNI decides which certificate Cloudflare serves:

    Terminal
    echo | openssl s_client -connect hub.yourdomain.com:443 -servername hub.yourdomain.com 2>/dev/null \
      | openssl x509 -noout -issuer -ext subjectAltName

    If a two-label hostname is unavoidable, Advanced Certificate Manager with a wildcard pack covers it. Prefer a wildcard over Total TLS, which issues per hostname only after the record appears, so every new app host would fail TLS for minutes.

    Published applications

    The infrastructure provider publishes apps as Gateway API HTTPRoutes. With the cfgate controller, point the provider at its Gateway (operator.application.gateway.name / namespace) and give each app zone a proxied wildcard record; cfgate syncs a proxied record per route, which wins over the wildcard. See Self-host Infrastructure .

    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 railgrid-system describe certificate
    kubectl -n railgrid-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