Self-hosted service
adyanth/cloudflare-operator avatar
adyanth/cloudflare-operator

cloudflare-operator: tunnels and DNS records as Kubernetes resources

A Kubernetes Operator to create and manage Cloudflare Tunnels and DNS records for (HTTP/TCP/UDP*) Service Resources

692 stars70 forksGoApache-2.0

At a glance

What is it?
The cloudflare-operator turns Cloudflare Tunnels into a Kubernetes custom resource, so an ingress route becomes a Tunnel plus a proxied DNS record and then disappears when the binding is deleted. It is an alpha project built with operator-sdk, it is not backed by Cloudflare, and it makes the cloudflared Deployment restart every time a route changes.
Who is it for?
Adopt cloudflare-operator if you run Kubernetes and want tunnel routes and the DNS records behind them to live in the cluster alongside the services, because the binding resource plus finalizer cleanup removes a class of manual DNS housekeeping. Do not adopt it if you need a supported operator, since the project states it is not backed by Cloudflare and is currently in alpha, and do not adopt it if your environment cannot tolerate cloudflared restarting on every route change.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 68 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The bare bones manifest the operator replaces

The motivation section is the clearest statement of the problem. The Cloudflare Tunnels guide for deployment on Kubernetes provides a manifest that is very bare bones and does not hook into Kubernetes in any meaningful way. The operator started out as a hobby project of the author's to deploy applications in a home lab and expose them to the internet via Cloudflare Tunnels without doing a lot of manual work every time a new application is deployed.

That origin explains both the design and the state of the project. The goal is to remove the repeated manual work, not to be a general purpose Cloudflare controller, and the surface is two custom resources rather than a dozen.

The project is scaffolded and built using operator-sdk, which is visible in the repository layout: there is a PROJECT file, an api/ directory for the custom resource types, a cmd/ directory, internal/ for the controller implementation, a config/ directory with the standard operator-sdk CRD and RBAC manifests, a bundle/ directory for the OperatorHub bundle, a hack/ directory, and a Dockerfile plus a bundle.Dockerfile.

The status note is unambiguous: this project is currently in Alpha. A second note records that UDP support for Cloudflare Tunnels is in Early Access at Cloudflare, and the description of the operator itself marks the protocol support as HTTP, TCP and UDP with an asterisk pointing at that note.

Two resources, one tunnel and one binding

The overview section describes two capabilities.

The first is the ability to create new and use existing Tunnels for Cloudflare for Teams using Custom Resources. Those resources accept a Secret for Cloudflare API Tokens and Keys, run a scaled and configurable Deployment of cloudflared, manage a ConfigMap for that Deployment, and can have Cluster or Namespace scoped Tunnels.

The second is the TunnelBinding controller, and this is where the actual reconciliation work lives. It updates the cloudflared ConfigMap to include the new Services to be served under a given Tunnel. It restarts the cloudflared Deployment to make the configuration change take effect. It adds a DNS entry in Cloudflare for the specified domain to be a proxied CNAME to the referenced tunnel. And it reverses the above when the TunnelBinding is deleted using Finalizers.

The finalizer detail is the part that makes this usable rather than merely clever. A Cloudflare DNS record does not disappear when the Kubernetes object that created it is deleted, so a naive controller leaks DNS entries into the account forever. Registering a finalizer on the binding means the object stays in Terminating state until the controller has removed the DNS record and cleaned up the ConfigMap, and the deletion is then retried until it succeeds.

The Cluster and Namespace scoping of the Tunnel resource is the other decision with consequences. A Cluster-scoped tunnel is shared by workloads across the cluster, which is what you want for a shared ingress, while a Namespace-scoped one keeps a team's tunnel inside its own namespace. The price of the shared case is that the ConfigMap for that tunnel is shared too, so the controller has to merge entries rather than replace them, and the restart it triggers affects every workload served by that tunnel.

The Go dependency set tells you what it talks to

The module manifest is short enough to read in full, and it describes the operator's integration surface precisely. The direct dependencies are cloudflare-go at v0.115.0 for the API, the logr and zap pair for logging, ginkgo and gomega for tests, yaml for parsing, and the Kubernetes client libraries with controller-runtime at v0.20.4:

toml
require (
	github.com/cloudflare/cloudflare-go v0.115.0
	github.com/go-logr/logr v1.4.2
	github.com/onsi/ginkgo/v2 v2.23.4
	github.com/onsi/gomega v1.37.0
	go.uber.org/zap v1.27.0
	gopkg.in/yaml.v3 v3.0.1
	k8s.io/api v0.33.0
	k8s.io/apimachinery v0.33.0
	k8s.io/client-go v0.33.0
	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e
	sigs.k8s.io/controller-runtime v0.20.4
	sigs.k8s.io/yaml v1.4.0
)

The module requires Go 1.24.0 with a toolchain directive of go1.24.2, and the Kubernetes libraries are on the 0.33 line. Because the CRDs and the controller are compiled against a specific Kubernetes API version, a cluster running an older control plane is a real constraint rather than a theoretical one.

The cloudflare-go dependency is the piece that decides how quickly the operator breaks when Cloudflare changes its API. The note in the README says the operator utilises the v4 API and the cloudflared daemon. Cloudflare's API is versioned and the operator pins a specific client library version, so an API change upstream shows up here as a dependency bump.

The container image is distroless and cross-platform

The Dockerfile shows a two-stage build. The builder stage starts from golang:1.24, takes TARGETOS and TARGETARCH as build arguments, copies the module manifests first so that the dependency layer is cached independently of the source, and then copies only cmd/main.go, api/ and internal/ before compiling with CGO disabled.

The cross-compilation comment in the file explains the design choice in unusual detail. The GOARCH has no default value so that the binary is built for the host on which the command was called, and the reasoning is spelled out: for a local make docker-build on an Apple Silicon machine the docker BUILDPLATFORM argument will be linux/arm64 while for an Apple x86 machine it is linux/amd64, so by leaving the value empty the container and the binary shipped on it are guaranteed to match. That is the kind of comment that only exists after someone shipped a broken image.

The runtime stage is gcr.io/distroless/static:nonroot with the manager binary copied in, the working directory set to the root, and the user set to the numeric 65532:65532. The entrypoint is the manager binary itself.

A distroless base means no shell in the image, no package manager and effectively nothing beyond the binary and the certificates it needs. For a controller that talks to the Kubernetes API over TLS that is a sensible hardening choice, and it also means you cannot exec into the pod and poke around when something is wrong, which is a real operational difference from a debugging-oriented image.

Alpha, unbaked, and upstream of Cloudflare's own plans

The release history shows what has changed and how fast. v0.12.0 introduced the AccessTunnel Controller on 2025-04-28, v0.13.0 followed on 2025-05-06 with a breaking change to v1alpha2, AccessTunnel support and more, and v0.13.1 on 2025-06-08 was a bug fix for a deployment update after a ConfigMap change. The repository is not archived and the last push was on 2026-08-15.

That ConfigMap bug fix is a small window into how the operator works. A change to the ConfigMap means the cloudflared Deployment has to be updated, and the fix was needed to make the deployment update correctly after that change. The mechanism of restarting the deployment on every configuration change is simple and reliable, and it is also the operator's main operational cost.

The second warning in the README is about provenance, and it should be read before anything else: this is not an official operator provided or backed by Cloudflare Inc. It utilises their v4 API and their cloudflared to automate setting up tunnels on Kubernetes. The licence is Apache-2.0, and the project is maintained by an individual author with a blog post describing the architecture and the thought process behind it.

For a team, that means the operator sits in the gap between a home lab and a supported product. It saves real work compared to hand-editing manifests, and it depends on Cloudflare's API continuing to behave as the client library expects, with nobody on Cloudflare's side contractually obliged to keep it working.

Where the operator is the wrong choice

The first limitation is the restart. Every change to the cloudflared ConfigMap restarts the Deployment, and the Deployment is where the cloudflared pods run. A shared tunnel with a ConfigMap that several teams add routes to will restart when any one of them changes their configuration, and a busy cluster will feel that. The alternative approach, where cloudflared watches its configuration and reloads without a pod restart, is not what this operator does, and the trade was made in exchange for a simple and predictable reconciliation loop.

The second limitation is the credential model. The Tunnel resource accepts a Secret for Cloudflare API Tokens and Keys, which means a long-lived credential with write access to DNS and tunnels sits in the cluster. There is no per-resource scoping described in the documentation, so the practical advice is to issue a token with the narrowest permissions that still work and rotate it.

The third limitation is the Alpha status combined with the v1alpha2 API version. Alpha means the custom resource definitions can change shape between releases, and a CRD upgrade is not a kubectl apply away from painless, because stored objects have to keep parsing against the new schema. The breaking change in v0.13.0 is exactly that kind of transition.

The real alternative is the thing the operator was written to replace, and it is not a bad option. A static manifest per application, applied with kubectl or Helm, gives you versioned configuration in Git, no controller, no Secret containing an API token, and no restarts. For a small number of tunnels in a home lab or a small team, that is often less total work than operating a controller, and it is easier to debug when it breaks. The operator wins when the number of services is large enough that hand-maintaining manifests becomes the bottleneck, and when the DNS records have to be created and destroyed reliably as part of the deployment lifecycle rather than by hand.

Editorial conclusion

Adopt cloudflare-operator if you run Kubernetes and want tunnel routes and the DNS records behind them to live in the cluster alongside the services, because the binding resource plus finalizer cleanup removes a class of manual DNS housekeeping. Do not adopt it if you need a supported operator, since the project states it is not backed by Cloudflare and is currently in alpha, and do not adopt it if your environment cannot tolerate cloudflared restarting on every route change. Before you install it, read the getting started guide in docs/ for the CRD installation, check the API version the CRDs require against your cluster, and confirm that the Cloudflare API token in the Secret has only the DNS and tunnel permissions the operator actually calls.

Frequently asked questions

What does the cloudflare-operator do?

It is a Kubernetes Operator to create and manage Cloudflare Tunnels and DNS records for HTTP, TCP and UDP service resources. It uses Custom Resources to run a scaled Deployment of cloudflared with a managed ConfigMap, and a TunnelBinding controller that updates the ConfigMap, restarts the Deployment, and adds a proxied DNS record for the specified domain.

Is the cloudflare-operator an official Cloudflare project?

No. The README states that it is not an official operator provided or backed by Cloudflare Inc, and that it utilises their v4 API and their cloudflared to automate setting up tunnels on Kubernetes. The project is Apache-2.0 licensed and is maintained by an individual author, and the README also notes the project is currently in Alpha.

How does the operator clean up Cloudflare DNS records?

The TunnelBinding controller reverses what it created when the binding is deleted, using Finalizers. The finalizer keeps the Kubernetes object in a terminating state until the DNS entry and the ConfigMap changes are removed, and deletion is retried until it succeeds, which stops orphaned DNS records accumulating in the Cloudflare account.

Which Kubernetes and Go versions does the cloudflare-operator target?

The module requires Go 1.24.0 with a toolchain of go1.24.2, and the Kubernetes libraries are on the 0.33 line with controller-runtime at v0.20.4. The container build uses golang:1.24 as the builder image and gcr.io/distroless/static:nonroot as the runtime base.

Does the cloudflare-operator support UDP?

The operator description marks UDP with an asterisk, and the note explains that UDP support for Cloudflare Tunnels is in Early Access. The documentation describes the operator as covering HTTP, TCP and UDP, with the UDP caveat pointing at Cloudflare's early access announcement.

Official sources

  1. adyanth/cloudflare-operator on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/adyanth-cloudflare-operator.svg)](https://hysenlabs.com/projects/adyanth-cloudflare-operator)

Was this useful?