A Kubernetes Gateway API implementation using Cloudflare Tunnels (cloudflared).
This controller implements the Kubernetes Gateway API to expose services through Cloudflare Tunnels without requiring public IPs or open inbound firewall ports. It automatically provisions Cloudflare Tunnels, manages DNS records, and configures cloudflared to route traffic to your Kubernetes services based on HTTPRoute definitions.
- Native Kubernetes Gateway API support (v1.0.0)
- GatewayClass, Gateway, and HTTPRoute resources
- Automatic Cloudflare Tunnel provisioning via API
- Automatic DNS record management in Cloudflare
- Dynamic cloudflared configuration updates
HTTPRoute → Gateway → cloudflared Deployment → Cloudflare Tunnel → Cloudflare Edge → Internet
The controller:
- Watches Gateway API resources (GatewayClass, Gateway, HTTPRoute)
- Provisions cloudflared deployments with Cloudflare Tunnel configuration
- Creates/manages Cloudflare Tunnels via API
- Automatically creates/updates DNS records for HTTPRoute hostnames
- Generates cloudflared ingress rules from HTTPRoute path-based routing
- Kubernetes cluster (1.27+)
- kubectl configured
- Cloudflare account with:
- API token with permissions for Tunnels and DNS
- Account ID
- At least one domain/zone in Cloudflare
The controller is deployed with Kustomize; the manifests live in
config/ in this repository. Everything below assumes the default namespace
cloudflare-gateway-system.
Skip this if they are already installed in the cluster.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yamlApply a tagged release straight from GitHub:
kubectl apply -k 'github.com/georgepstaylor/cloudflare-gateway-controller/config/default?ref=v0.3.2' # x-release-please-versionThis creates the namespace, ServiceAccount, ClusterRole, ClusterRoleBinding and the controller Deployment.
The controller reads credentials from a Secret referenced by the GatewayClass's
parametersRef, so create it yourself rather than committing it:
kubectl create secret generic cloudflare-credentials \
--namespace cloudflare-gateway-system \
--from-literal=api-token=YOUR_API_TOKEN \
--from-literal=account-id=YOUR_ACCOUNT_IDkubectl apply -f https://raw.githubusercontent.com/georgepstaylor/cloudflare-gateway-controller/v0.3.2/config/samples/gatewayclass.yaml # x-release-please-versionSample Gateway and HTTPRoute manifests are in config/samples/.
Earlier versions were installed from a Helm chart, which has been replaced by the
manifests in config/. To move across:
helm uninstall cloudflare-gateway-controller -n cloudflare-gateway-systemThen follow the steps above. Uninstalling the release deletes the Secret it generated, so recreate it with step 3; your Gateways and HTTPRoutes are untouched, and the controller picks them back up on startup.
Note that the chart always passed a --cluster-domain flag, which no longer exists - the
controller now relies on cluster DNS search paths instead. Replace the Deployment with
these manifests rather than only bumping the image tag, or the manager will exit with
flag provided but not defined: -cluster-domain.
Reference this repository as a base from your own kustomization and patch what you need. Every knob the Helm chart exposed is either a direct kustomize equivalent or no longer needed:
| Chart value | Kustomize equivalent |
|---|---|
image.repository / image.tag |
images: transformer |
imagePullPolicy |
strategic merge patch on the container |
replicaCount |
built-in replicas: field |
resources |
strategic merge patch on the container |
podAnnotations |
strategic merge patch on the pod template |
imagePullSecrets |
strategic merge patch on the pod spec |
nodeSelector / tolerations / affinity |
strategic merge patch on the pod spec |
podSecurityContext / securityContext |
strategic merge patch |
nameOverride / fullnameOverride |
namePrefix: / nameSuffix: |
serviceAccount.name / .annotations |
patch the ServiceAccount (and its ClusterRoleBinding subject) |
cloudflare.apiToken / .accountId |
the Secret you create in step 3 |
cloudflare.existingSecret, gatewayAPI.installCRDs and gatewayAPI.version were never
wired into the chart's templates and have no equivalent here. The Secret name is whatever
your GatewayClass parametersRef points at, and the Gateway API CRDs are installed in
step 1.
cluster.domain is gone too, and needs no replacement: backend URLs are emitted as
service.namespace.svc, which cloudflared's resolv.conf search path completes for
whatever DNS domain your cluster uses. Clusters on a custom domain work with no
configuration at all.
A kustomization exercising most of the above:
# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cloudflare-gateway-system
resources:
- github.com/georgepstaylor/cloudflare-gateway-controller/config/default?ref=v0.3.2 # x-release-please-version
images:
- name: ghcr.io/georgepstaylor/cloudflare-gateway-controller
newTag: 0.3.2 # x-release-please-version
replicas:
- name: cloudflare-gateway-controller
count: 2
patches:
# Scheduling, annotations, pull secrets and resources in one merge patch.
- patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflare-gateway-controller
spec:
template:
metadata:
annotations:
prometheus.io/scrape: "true"
spec:
imagePullSecrets:
- name: ghcr-credentials
nodeSelector:
kubernetes.io/arch: arm64
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: manager
imagePullPolicy: Always
resources:
limits:
cpu: 1
memory: 256MiTo deploy from a checkout instead:
make deploy # kubectl apply -k config/default
make render # print the rendered manifests
make undeploy # remove it againkubectl delete -k 'github.com/georgepstaylor/cloudflare-gateway-controller/config/default?ref=v0.3.2' # x-release-please-versionGateways created by the controller are cleaned up (and their Cloudflare Tunnels deleted) when you delete the Gateway resources, so delete those before removing the controller.
- Validates controller ownership (
controllerName) - References a Secret containing Cloudflare API credentials
- Checks the credentials Secret exists and has
api-tokenandaccount-idset (it does not call the Cloudflare API, so an invalid token is only discovered when a Gateway is reconciled) - Sets
Acceptedstatus condition
- Creates a new Cloudflare Tunnel via API
- Stores tunnel metadata (ID, secret, name) in Gateway annotations
- Creates a Secret with tunnel credentials JSON
- Creates a ConfigMap with cloudflared configuration YAML
- Deploys cloudflared as a Kubernetes Deployment (2 replicas by default)
- Sets Gateway status with tunnel hostname (
{tunnel-id}.cfargotunnel.com) - Handles cleanup: deletes tunnel from Cloudflare when Gateway is deleted
- Watches parent Gateway references
- Extracts hostnames and path-based routing rules
- Builds cloudflared ingress rules mapping hostnames/paths to backend Services
- Updates Gateway's ConfigMap with new ingress configuration
- Creates/updates DNS CNAME records in Cloudflare for each hostname
- Sets HTTPRoute status with parent Gateway conditions
# Build binary
make build
# Run locally (requires kubeconfig)
make run
# Run tests
make test
# Format and vet code
make fmt vet# Build image
make docker-build IMG=your-registry/cloudflare-gateway-controller:tag
# Push image
make docker-push IMG=your-registry/cloudflare-gateway-controller:tagkubectl logs -n cloudflare-gateway-system deployment/cloudflare-gateway-controller -fkubectl logs -l app=cloudflared -fGateway not becoming Ready:
- Check GatewayClass is
Accepted - Verify Cloudflare credentials are valid
- Check controller logs for API errors
HTTPRoute not working:
- Verify backend Service exists and has endpoints
- Check Gateway is
Programmed - Verify DNS records were created in Cloudflare dashboard
- Check cloudflared logs for routing errors
DNS records not created:
- Ensure API token has DNS edit permissions
- Verify zone exists in Cloudflare account
- Check HTTPRoute status conditions
- Only supports HTTPRoute (no TCPRoute, UDPRoute, TLSRoute yet)
- One backend per rule (no weighted load balancing yet)
- PathPrefix and Exact path match types supported
- Cloudflare handles TLS termination at the edge
Contributions welcome! This is a learning project for understanding Kubernetes controllers and the Gateway API.
Apache 2.0
Built with: