# cloudflare-tunnel-ingress-controller: publishing Ingress through a Cloudflare Tunnel

> This Kubernetes ingress controller reconciles Ingress resources into Cloudflare Tunnel configuration and DNS records, so a service becomes reachable from the internet without a public load balancer. It is MIT licensed, written in Go, installed with Helm, and still on a 0.1.0 tag.

**STRRL/cloudflare-tunnel-ingress-controller** — 🚀 Expose the website directly into the internet! The Kuberntes Ingress Controller based on Cloudflare Tunnel.

- Repository: https://github.com/STRRL/cloudflare-tunnel-ingress-controller
- Website: https://tunnel.strrl.dev
- Stars: 1,203 · Forks: 93
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/strrl-cloudflare-tunnel-ingress-controller

## Ingress in, tunnel routes out

The cloudflare-tunnel-ingress-controller is a Kubernetes ingress controller built on Cloudflare Tunnel, and its stated purpose is to simplify exposing Kubernetes services to the internet easily and securely using Cloudflare Tunnel.

The mechanism is the standard Kubernetes one with a different ending. You write an Ingress resource, the controller reads it, and instead of provisioning a load balancer or creating an Ingress on a cloud provider, it writes tunnel configuration and DNS records on Cloudflare. The result is that a service in the cluster is reachable from the internet, and the cluster itself does not need to accept inbound connections.

That last point is the security property. A tunnel is an outbound connection from inside the cluster to Cloudflare's edge, so the cluster does not have to expose ports to the internet at all. The README's prerequisites make that explicit, requiring a Kubernetes cluster with public Internet access rather than a cluster with public ingress.

The alternative arrangement is named in the README, and it is a real one to weigh. The adyanth/cloudflare-operator project could integrate with Cloudflare Tunnel as a CRD. The difference in approach is the interface: this controller speaks the Ingress resource that every ingress controller speaks, so it drops into an existing cluster with the same annotations and the same tools, while a CRD-based operator introduces a new resource type that only that operator understands.

The project is MIT licensed, written in Go with the module path `github.com/STRRL/cloudflare-tunnel-ingress-controller`, and the documentation site is at tunnel.strrl.dev. The last push was on 2026-10-11, and the release tags are v0.1.0-rc.0, v0.1.0-rc.1 and v0.1.0, all in August 2026.

## The API token permissions, spelled out

The prerequisites are the part of the README that tells you exactly what the controller does on your Cloudflare account, and it is worth reading precisely.

You need a Cloudflare account, a domain configured on Cloudflare, and an API token with three permissions: `Zone:Zone:Read`, `Zone:DNS:Edit` and `Account:Cloudflare Tunnel:Edit`. The README says these can be created quickly using a template that the documentation links to, and the template URL encodes those same three permission groups. You also need to fetch the Account ID from the Cloudflare dashboard.

The three permissions map onto the three things the controller does. Reading the zone tells it which zone a hostname belongs to, editing DNS is what lets it create the CNAME record that points the hostname at the tunnel, and editing the tunnel is what lets it write the ingress rules for that tunnel.

That last one is the permission to think about, because editing a tunnel configuration is not a read-only operation. The token is scoped to tunnels, so it cannot change other Cloudflare settings, but it can change how traffic enters the account.

The credentials are then read from a Kubernetes Secret rather than from a flag, and the README is precise about the failure mode. The controller pod expects a Secret named `cloudflare-api`, and without it the pod fails with `CreateContainerConfigError: secret "cloudflare-api" not found`. Two ways of providing it are documented, a kubectl command that creates the secret from literals in the development namespace, and copying an example file at `hack/dev/cloudflare-api.example.yaml` to `hack/dev/cloudflare-api.yaml` and filling in your own values, which is the recommended path for local development because that file is in `.gitignore` and so your credentials are not committed.

## Installing with Helm

The controller is installed with a Helm chart from the project's own repository, and the install command shows the shape of the configuration:

```bash
helm upgrade --install --wait \
  cloudflare-tunnel-ingress-controller \
  cloudflare-tunnel-ingress-controller \
  --repo https://helm.strrl.dev \
  --namespace cloudflare-tunnel-ingress-controller \
  --create-namespace \
  --set cloudflare.apiToken="<CLOUDFLARE_API_TOKEN>" \
  --set cloudflare.accountId="<CLOUDFLARE_ACCOUNT_ID>" \
  --set cloudflare.tunnelName="<TUNNEL_NAME>"
```

Three things are worth noticing in that command. It is `upgrade --install` rather than `install`, so the same command works for the first install and for later changes, and it uses `--wait` so Helm does not return until the resources are ready. And the three settings it takes are exactly the three pieces of Cloudflare state the controller needs: the API token, the account, and the name of the tunnel it should manage.

The chart repository is the project's own, at helm.strrl.dev, rather than a public chart registry, and the namespace and the `--create-namespace` flag are part of the command because the controller expects to run in its own namespace.

Configuration beyond the install is done through CLI flags and matching environment variables, with the complete list and defaults in the controller configuration reference in the documentation.

The README also links a short video showing the whole thing working, described as bootstrapping a Kubernetes cluster and exposing the Kubernetes dashboard to the internet in under four minutes. That is a useful signal about the intended experience, and the quickstart guide is where you publish your first Ingress.

## Monitoring with Grafana dashboards

There are two ready-made Grafana dashboards, published on the Grafana dashboard catalogue, one for the controller and one for cloudflared traffic. The README links them as dashboards 25659 and 25660, and the setup instructions are in the monitoring page of the documentation.

The pairing is the sensible one. The controller dashboard tells you what the controller is doing, and the cloudflared dashboard tells you what the tunnel is carrying. A tunnel that is up but not serving traffic looks identical from the inside of the cluster to a tunnel that is not running, so the traffic side of the picture is the one that catches real outages.

The dashboards are not written by hand in the repository. The Makefile has a `dashboards` target that compiles them from jsonnet sources into plain files under `mixin/dist`, using the `jsonnet` binary, with one source per dashboard, `mixin/dashboards/controller.jsonnet` and `mixin/dashboards/cloudflared.jsonnet`. Generating them from jsonnet means the same dashboard definition can be rendered in several forms, which is the standard reason to use jsonnet at all.

The repository has a `mixin/` directory for that, a `static/` directory, and the Prometheus client library is in the dependency list, so the metrics come from the controller itself rather than from a sidecar. The Go module also includes `sigs.k8s.io/controller-runtime` at 0.25.2 and the Kubernetes libraries at v0.37.1, which is the standard controller stack.

## Local development with Skaffold and Act

The development workflow is documented with a focus on running the same code the CI runs, which is a good sign for a project this young.

For the GitHub Actions workflows, the README suggests Act, which runs those workflows locally. The example given is running the unit tests through the same workflow file CI uses:

```bash
act -W .github/workflows/unit-test.yaml
```

All the available workflows are in the `.github/workflows` directory, and the Makefile is where the local targets live.

For running the project itself, Skaffold is integrated into the Makefile. The `dev` target runs `skaffold dev` with the development namespace and `--cache-artifacts=false`, so the image is rebuilt rather than pulled from a cache. The `setup` target it depends on installs prek, a pre-commit hook runner, and the Makefile fails with a pointer to the prek installation page if it is not found.

The image target builds with `DOCKER_BUILDKIT=1` and `TARGETARCH=amd64` into a `ghcr.io/strrl/cloudflare-tunnel-ingress-controller` tag, and the Dockerfile lives at `image/cloudflare-tunnel-ingress-controller/Dockerfile`.

The test targets are more layered than a typical controller. The unit test target runs `go test -race` over `./pkg/...` with a coverage profile. The integration test target first fetches the envtest assets in its own shell step, with a comment explaining why, so that a failed download stops the build rather than running the tests with an empty `KUBEBUILDER_ASSETS`. The end-to-end target builds a separate image with `COVER=1` and a distroless debug base, then runs `test/e2e/e2e.sh`.

The dependency list also shows `chromedp`, a headless Chrome driver, and `cucumber/godog` with the gherkin parser, which is how the end-to-end tests drive a real browser to verify what Cloudflare actually serves.

## What the version 0.1.0 means for adoption

The release history is the thing to look at before deploying this, and it is short. There are three tags, v0.1.0-rc.0, v0.1.0-rc.1 and v0.1.0, published on 2026-08-09, 2026-08-11 and 2026-08-12 respectively, and the final release is a 0.1.0 with release candidates immediately before it.

A 0.1.0 is conventionally the signal that the interface is not settled, and the timing is recent, two months before the last push. What that means operationally is that the resource model, the annotations it reacts to and the configuration surface may still change between releases.

The counterweight is that the project is not abandoned. The last push was on 2026-10-11, and the repository is not archived. The commit history shows a project in active development rather than one that shipped and stopped.

The scope is also narrow in a way that is easy to accept. This is one controller for one path, Ingress resources to Cloudflare Tunnel, and it does not try to be a general Cloudflare operator. The docs directory in the tree and the `cmd/`, `pkg/`, `helm/` and `test/` directories match that shape, and the `hack/` directory holds the development helper scripts including the envtest installer.

There is also a small community signal in the README, which asks people to fill out a short survey about how the project works for them. For a project at 0.1.0 that is a reasonable thing to be doing, since the feedback from the first few dozen users is what shapes the interface before it stabilises.

## Conclusion

cloudflare-tunnel-ingress-controller fits a Kubernetes operator who wants the standard Ingress resource to drive Cloudflare Tunnel, and who prefers an outbound tunnel to inbound ports on the cluster. It does not fit a team that needs a proven, long-lived controller, since the newest tag is still 0.1.0 and the release history is only three tags long. Before adopting it, create the API token with exactly the three listed permissions, and note that the controller pod fails at startup without a Secret named cloudflare-api.

## FAQ

### What does the Cloudflare Tunnel Ingress Controller do?

It is a Kubernetes ingress controller that reconciles Ingress resources into Cloudflare Tunnel configuration and DNS records, so a service is exposed to the internet through an outbound tunnel. The cluster does not need to accept inbound connections. The project is MIT licensed and written in Go.

### Which Cloudflare API token permissions does the controller need?

Zone:Zone:Read, Zone:DNS:Edit and Account:Cloudflare Tunnel:Edit, which the README says can be created quickly from a template. You also need the Cloudflare Account ID from the dashboard and a domain configured on Cloudflare.

### How do I install the Cloudflare Tunnel Ingress Controller?

With Helm from the project's own chart repository, using `helm upgrade --install --wait` against https://helm.strrl.dev, in the cloudflare-tunnel-ingress-controller namespace, setting cloudflare.apiToken, cloudflare.accountId and cloudflare.tunnelName. The quickstart guide covers publishing your first Ingress.

### Why does the controller pod fail to start?

It expects a Kubernetes Secret named `cloudflare-api`, and without it the pod fails with `CreateContainerConfigError: secret "cloudflare-api" not found`. You can create it with kubectl or copy hack/dev/cloudflare-api.example.yaml to hack/dev/cloudflare-api.yaml and fill in your credentials, which is gitignored.

### How do I run the Cloudflare Tunnel Ingress Controller locally?

Skaffold is integrated into the Makefile, so `skaffold dev` starts the development environment in the cloudflare-tunnel-ingress-controller-dev namespace. The unit test workflow can also be run locally with `act -W .github/workflows/unit-test.yaml`.

## Sources

- [License: MIT](https://github.com/STRRL/cloudflare-tunnel-ingress-controller/blob/master/LICENSE)
- [Project website](https://tunnel.strrl.dev)
- [README](https://github.com/STRRL/cloudflare-tunnel-ingress-controller/blob/master/README.md)
- [Releases](https://github.com/STRRL/cloudflare-tunnel-ingress-controller/releases)
- [STRRL/cloudflare-tunnel-ingress-controller on GitHub](https://github.com/STRRL/cloudflare-tunnel-ingress-controller)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/strrl-cloudflare-tunnel-ingress-controller
