cfgate is a Kubernetes operator that manages Cloudflare Tunnels, DNS records, reusable Access policies, and Access application bindings through custom resources. It uses Gateway API, the CNCF standard replacing Ingress, so routing configuration works the same as Envoy, Istio, or Cilium. Clusters running cfgate need no public IP, no ingress controller, and no load balancer. Traffic reaches services through Cloudflare Tunnels: outbound-only connections from the cluster to Cloudflare's edge.
Gateway API is the Kubernetes successor to Ingress. If you're coming from Ingress, see the Gateway API Primer.
- Composable CRDs for tunnels, DNS, and access. CloudflareTunnel, CloudflareDNS, CloudflareAccessPolicy, and CloudflareAccessApplication each manage a distinct piece of Cloudflare infrastructure as Kubernetes resources. Tunnels, DNS records, reusable policies, and app bindings all live in version-controlled YAML instead of the Cloudflare dashboard.
- Outbound-only tunnel connections. Cloudflare Tunnels establish outbound-only connections from the cluster to Cloudflare's edge. Services are never exposed via public IP or load balancer.
- Built on Gateway API. Uses the Gateway API standard, not a proprietary abstraction. Existing community operators use the deprecated Ingress API and lack Access policy management.
- Independent, composable resources. Each CRD operates independently. Use the resources together or pick the ones you need: a tunnel without DNS sync, DNS without Access, Access policies without app bindings, or the full stack.
Define a CloudflareTunnel, point a Gateway at it, and attach HTTPRoutes to the Gateway. cfgate reconciles each resource against the Cloudflare API: it creates the tunnel, deploys cloudflared pods, syncs DNS records, syncs reusable Access policies, and binds Access Applications to route host/path targets. Traffic flows from Cloudflare's edge through the tunnel directly to in-cluster services. The cluster needs no public IP, no ingress controller, and no load balancer.
Kustomize
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
kubectl apply -f https://github.com/cfgate/cfgate/releases/latest/download/install.yamlHelm
helm install cfgate oci://ghcr.io/cfgate/charts/cfgate \
--namespace cfgate-system --create-namespaceBoth methods create the
cfgate-systemnamespace. CloudflareTunnel and CloudflareDNS resources typically live here. Routes and services can be in any namespace.
kubectl create secret generic cloudflare-credentials \
-n cfgate-system \
--from-literal=CLOUDFLARE_API_TOKEN=<your-token>apiVersion: cfgate.io/v1alpha1
kind: CloudflareTunnel
metadata:
name: my-tunnel
namespace: cfgate-system
spec:
tunnel:
name: my-tunnel
cloudflare:
accountId: "<account-id>"
secretRef:
name: cloudflare-credentials
cloudflared:
replicas: 2apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: cfgate
spec:
controllerName: cfgate.io/cloudflare-tunnel-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: cloudflare-tunnel
namespace: cfgate-system
annotations:
cfgate.io/tunnel-ref: cfgate-system/my-tunnel
spec:
gatewayClassName: cfgate
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: AllGatewayClass declares the controller (cfgate.io/cloudflare-tunnel-controller). Gateway is the runtime instance that binds to a specific CloudflareTunnel via the cfgate.io/tunnel-ref annotation. Both are required.
apiVersion: cfgate.io/v1alpha1
kind: CloudflareDNS
metadata:
name: my-dns
namespace: cfgate-system
spec:
tunnelRef:
name: my-tunnel
zones:
- name: example.com
source:
gatewayRoutes:
enabled: trueThe presence of source.gatewayRoutes enables route discovery. With enabled: true and no annotationFilter, cfgate syncs DNS records for all routes attached to the referenced tunnel's Gateways. Explicit-only CloudflareDNS resources do not watch routes. To sync specific routes only, use the annotationFilter field. See CloudflareDNS reference.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
namespace: default
spec:
parentRefs:
- name: cloudflare-tunnel
namespace: cfgate-system
hostnames:
- app.example.com
rules:
- backendRefs:
- name: my-service
port: 80cfgate automatically:
- Creates a CNAME record
app.example.com→{tunnelId}.cfargotunnel.com - Adds a cloudflared ingress rule routing
app.example.com→http://my-service.default.svc:80 - Manages ownership TXT records for safe multi-cluster deployments
CloudflareTunnel manages tunnel lifecycle and cloudflared deployment. A single tunnel serves any number of domains across zones. → Full reference
CloudflareDNS syncs DNS records independently from tunnel lifecycle, with multi-zone support and ownership tracking. → Full reference
CloudflareAccessPolicy manages reusable account-level Cloudflare Access policies. → Full reference
CloudflareAccessApplication binds reusable policies to Gateway and HTTPRoute host/path targets. → Full reference
Per-route configuration (origin protocol, TLS settings, timeouts, DNS TTL) is set via annotations on Gateway API HTTPRoute resources. → Full reference
| Document | Description |
|---|---|
| Gateway API Primer | Gateway API concepts for Ingress users |
| CloudflareTunnel | Full CRD reference |
| CloudflareDNS | Full CRD reference, annotationFilter, ownership |
| CloudflareAccessPolicy | Reusable Access policy reference, rule types, service tokens |
| CloudflareAccessApplication | Gateway API target binding, path rules, policyRefs |
| Annotations | Complete annotation reference |
| Service Mesh | Istio, Envoy Gateway, and Kiali integration |
| Troubleshooting | Diagnostic steps and solutions |
| Testing | Unit and E2E test strategy |
| Contributing | Development setup and workflow |
| Changelog | Release history |
| Example | Description |
|---|---|
| basic | Single tunnel + gateway + DNS sync |
| multi-service | Multiple services, one tunnel, reusable Access policies and app bindings |
| with-rancher | Rancher 2.14+ integration |
| external-target | A/AAAA records via ExternalTarget (no tunnel) |
Create a token at Cloudflare Dashboard → API Tokens with:
| Scope | Permission | Used By |
|---|---|---|
| Account | Cloudflare Tunnel: Edit | CloudflareTunnel |
| Account | Access: Apps and Policies: Edit | CloudflareAccessPolicy, CloudflareAccessApplication |
| Account | Access: Service Tokens: Edit | CloudflareAccessPolicy |
| Account | Account Settings: Read | CloudflareTunnel (accountName only)* |
| Zone | DNS: Edit | CloudflareDNS |
*Only required when using spec.cloudflare.accountName instead of accountId.
- Kubernetes 1.26+
- Gateway API v1.5.1+ CRDs installed
- cluster-admin access for CRD installation
| Repository | Description |
|---|---|
| cfgate/helm-chart | Helm chart for cfgate |
| cfgate/cfgate.io | Project website |
brew install mise
mise install
mise tasksSee CONTRIBUTING.md for full development setup, secrets configuration, and contribution guidelines.
See docs/TESTING.md for the unit-only CI coverage model, local-only E2E bootstrap paths, environment variables, and test execution.