Upgrading from v2.x to v3.0¶
v3 collapses the two data plane modes that the v1/v2 chart supported (a separately-managed cloudflared deployment plus an opt-in L7 proxy) into a single unified data plane: the L7 proxy binary embeds cloudflared transport in-process and is the only path tunnel traffic takes. This page lists the breaking changes and the steps to migrate.
What changed¶
- The chart no longer renders
proxy.enabled: false. The proxy Deployment, Services, NetworkPolicy and ServiceMonitor are always rendered;proxy.tunnelTokenSecretRef.nameis now mandatory — the chart's template-level{{ required "..." }}check intemplates/deployment-proxy.yamlfails the install when the value is empty. - The controller no longer manages a separate cloudflared deployment. All Helm SDK code paths inside the controller are gone — there is no longer an in-cluster Helm release named
cfd-<gateway>for each Gateway. cloudflared transport now runs inside the proxy pod, configured via the chart'sproxy.tunnelTokenSecretRef. - The GatewayClassConfig CRD is slimmer.
tunnelTokenSecretRefand the entirecloudflaredblock (enabled,replicas,namespace,protocol,awg,livenessProbe) have been removed from the spec. Proxy-side configuration moves to chart values. --proxy-endpointsis required at controller startup. The bootstrap fails fast with a clear error if the flag is empty.
Migration steps¶
-
Apply the v3 CRD BEFORE
helm upgrade. Helm 3'scrds/directory installs CRDs only on the firsthelm install;helm upgradedeliberately never touches them. Without this step the v2 CRD's CEL validation (tunnelTokenSecretRef is required when cloudflared.enabled is true) fails the v3 template's stripped GatewayClassConfig, and the upgrade aborts with a confusingadmission webhook denied the requesterror.# Apply the v3 CRD shipped with this release (replace <tag> with the v3.x.y # version you're upgrading to). kubectl apply --filename https://raw.githubusercontent.com/lexfrei/cloudflare-tunnel-gateway-controller/<tag>/charts/cloudflare-tunnel-gateway-controller/crds/cf.k8s.lex.la_gatewayclassconfigs.yamlThe v3 CRD drops the CEL rule that mentioned
cloudflared.enabledandtunnelTokenSecretRef; the rendered v3GatewayClassConfigthen validates cleanly. -
Replace
gatewayClassConfig.cloudflared.*andgatewayClassConfig.tunnelTokenSecretRefwith proxy-side equivalents. Move the tunnel token Secret reference from the CRD into the chart values:# before (v2) gatewayClassConfig: tunnelTokenSecretRef: name: cloudflare-tunnel-token cloudflared: enabled: true replicas: 2 # after (v3) gatewayClassConfig: # spec now only carries cloudflareCredentialsSecretRef, accountId, tunnelID proxy: tunnelTokenSecretRef: name: cloudflare-tunnel-token replicas: 2 -
Drop
proxy.enabled: falseif you ever set it. v2 users who ran the controller with the L7 proxy disabled need to setproxy.tunnelTokenSecretRef.namebefore upgrading, otherwise the chart install fails on the required check. The proxy is the only data plane in v3.Use
--reset-then-reuse-valuesonhelm upgradeThe v3 chart introduces a required value (
proxy.tunnelTokenSecretRef.name) that the v2 defaults didn't carry.helm upgrade --reuse-valuesonly re-applies the user overrides from the previous release and drops new chart defaults — so the install fails with the chart'srequirederror. Pass--reset-then-reuse-values(Helm 3.14+) so new defaults merge under your overrides. -
Clean up the legacy in-cluster cloudflared releases, if any. The controller no longer reconciles them, but a leftover
cfd-<gateway>Helm release will keep an orphaned cloudflared Deployment running. Discover them and uninstall: -
Make sure the controller deployment passes
--proxy-endpoints. The chart wires this unconditionally — only out-of-tree deployments that ran the controller binary directly need to add the flag. The expected value points at the proxy's headless Service (http://<fullname>-proxy-headless.<namespace>.svc.<cluster-domain>:<proxy.configAPIPort>/config), where<fullname>is the Helm release fullname (typically<release>-cloudflare-tunnel-gateway-controller, or just<release>when the release name already contains the chart name), truncated so the suffixed Service name fits the 63-character DNS label limit. To read the exact name your release rendered, runhelm get manifest <release> | grep -m1 'proxy-headless'. -
No data migration is required for CRs. The Kubernetes API server prunes unknown fields when you apply the new CRD schema (the v3 CRD does not set
x-kubernetes-preserve-unknown-fields: true, so apiextensions/v1's default pruning applies), so existing GatewayClassConfig resources continue to work — the removedcloudflaredandtunnelTokenSecretReffields are silently dropped on next read/write. -
Legacy finalizer cleanup is automatic on delete. The v2 controller attached a
cloudflare-tunnel.gateway.networking.k8s.io/cloudflaredfinalizer to every Gateway it reconciled. The v3 controller does not strip the finalizer from live Gateways — it sits there harmlessly until the Gateway is actually deleted, at which point the deletion path removes it on the first reconcile and the Gateway proceeds with normal termination.If you want to strip it proactively without deleting the Gateway, use a conditional JSON-patch that aborts if the finalizer is not at the expected index — never a bare indexed
removethat can silently delete the wrong finalizer if the list is reordered between your look-up and the patch:# Inspect the current finalizer list first. kubectl get gateway <name> -n <ns> -o json | jq '.metadata.finalizers' # Replace 0 with the index returned above. The `test` op makes the # patch fail loudly if the index does not still point at the legacy # finalizer, so the `remove` op cannot delete the wrong entry. kubectl patch gateway <name> -n <ns> --type=json -p='[ {"op":"test","path":"/metadata/finalizers/0","value":"cloudflare-tunnel.gateway.networking.k8s.io/cloudflared"}, {"op":"remove","path":"/metadata/finalizers/0"} ]'Alternatively, when the Gateway has no other finalizers (or you want to drop them all), use a merge patch that rewrites the list without the legacy entry — caveat: this replaces the entire list, so include any other finalizers you want to preserve:
GRPCRoute routing¶
v2 (default) routed gRPC traffic via cloudflared's native ingress. v3 collapses everything to the L7 proxy. Early v3 builds had no gRPC matcher and returned 404 no matching route; current v3 serves GRPCRoute through the proxy — gRPC service/method matches map onto /{service}/{method} path rules and the upstream hop is h2c. No migration is needed for GRPCRoute resources; they route as before. See GRPCRoute.
AmneziaWG sidecar is gone¶
The AmneziaWG sidecar was a feature of the legacy cloudflared-managed-by-controller path: the controller's Helm SDK render of cloudflared wired in an AWG sidecar that intercepted the cloudflared egress. v3 has no separate cloudflared deployment, no Helm SDK render, and no sidecar slot on the proxy pod, so AWG is no longer offered as a built-in option. If you relied on AWG to obfuscate the tunnel transport, stay on the v2.x chart line until upstream re-introduces an equivalent.
Why this is a breaking change¶
The v2 chart supported two independent ways to terminate Cloudflare Tunnel traffic, and both were on by default. The L7 proxy was the path that actually receives Gateway API features (regex matching, filters, URL rewrites, CORS), so leaving the legacy cloudflared-only mode in place mostly led to silent feature gaps when users discovered their HTTPRoute filters were not being honoured. Collapsing to a single data plane removes the foot-gun and lets the controller's status reporting match what the data plane is actually doing.
Staying on v2¶
The v2.x chart line continues to receive critical fixes for a period. If you cannot migrate yet, pin the chart version and watch the v2 release notes for the cut-off date.