k8wiz
Back to Articles

Ready to test your Flux CD knowledge? Try our Flux CD quiz!

Upgrading to Flux 2.10: breaking changes and a safe upgrade path

What Flux 2.10 changes, which clusters break on upgrade (token RBAC, notification v1, image tag storage) and the order of steps that avoids it.

10 min read
By k8wiz Team
  • fluxcd
  • upgrade
  • gitops
  • release

Flux 2.10.0 came out on 26 October 2026. It is a feature release, and for most clusters the upgrade is the usual routine of regenerating the components manifest and letting Flux apply it. A few setups break, though, and they break quietly. Sources stop authenticating, alerts lose their summary, and image automation rescans every registry.

This guide covers what changed, how to tell whether your cluster is affected, and an upgrade order that keeps everything running. It assumes you run Flux 2.9 today, installed with flux bootstrap or with the components manifest committed to Git.

What ships in 2.10

The release bundles source-controller v1.10.1, kustomize-controller v1.10.1, notification-controller v1.10.0, helm-controller v1.7.1, image-reflector-controller v1.3.0, image-automation-controller v1.3.0 and source-watcher v2.3.1. Everything is built with Kubernetes 1.37 and Go 1.27, and helm-controller uses Helm 4.3.1.

Flux supports the three latest Kubernetes minor versions, so 2.10 runs on 1.35, 1.36 and 1.37. The release notes set patch minimums too, 1.35.2 and 1.36.1. If you are still on 1.34, upgrade Kubernetes first.

The headline feature is object identity. In 2.9 it was called object-level workload identity and sat behind the ObjectLevelWorkloadIdentity feature gate. It lets each Flux object authenticate to a cloud service or registry as its own Kubernetes ServiceAccount, instead of sharing the controller's identity. In 2.10 it is generally available, the gate is retired and the feature is always on. The notification APIs (Event, Alert and Provider) also reach v1.

The four breaking changes

Token creation is no longer in the default RBAC

Object identity works by asking the Kubernetes TokenRequest API for a token on behalf of the ServiceAccount named in the Flux object. That requires the create verb on serviceaccounts/token. Up to 2.9, the crd-controller ClusterRole that Flux installs granted it to every controller. In 2.10 it is gone.

kustomize-controller and helm-controller are unaffected because they are bound to cluster-admin. source-controller, notification-controller and the two image controllers are not. If any of your GitRepository, OCIRepository, Bucket, ImageRepository or notification Provider objects use spec.serviceAccountName to get cloud credentials, they will start failing as soon as the new RBAC lands. This is the change most likely to page someone, and it has to be handled before the upgrade.

Alert and Provider move to v1

notification.toolkit.fluxcd.io/v1 now serves Alert and Provider, and it is the storage version. v1beta3 is still served but deprecated. The catch is in conversion. A few deprecated fields don't exist in v1, so they are dropped when an object is converted. These are Alert.spec.summary, Provider.spec.interval, Provider.spec.proxy, the static JWT for Azure Event Hub, and TLS or proxy settings stored in the Secret behind Provider.spec.secretRef. Their replacements are spec.eventMetadata.summary, spec.proxySecretRef and spec.certSecretRef.

Nothing errors when a field is dropped. Your Slack messages just lose their summary line, or a Provider behind a corporate proxy stops reaching the internet.

Image tags move to the artifact storage backend

image-reflector-controller used to keep scanned tags in BadgerDB. In 2.10 the FluxStorage feature gate is on by default, and tags go into the same artifact storage the other controllers use. Switching backends wipes the local tag database, so the first reconciliation after the upgrade rescans every ImageRepository. With a few repositories that is harmless. With hundreds, it is a burst of registry requests that can hit rate limits, Docker Hub's in particular.

Remote cluster auth uses object identity

Kustomizations and HelmReleases that deploy to another cluster through spec.kubeConfig.configMapRef now authenticate through object identity. If that ConfigMap has the legacy audiences key and you want to use the new ServiceAccount annotations, replace it with the credential.auth.fluxcd.io/audiences annotation on the ServiceAccount. The two can't be combined.

Step 1: check the CLI and the cluster

Install the 2.10 CLI on the machine that will run the upgrade, then run the pre-flight check against each cluster. flux check --pre verifies the Kubernetes version and tells you if a newer Flux is available.

flux --version
flux check --pre

The CLI has to be 2.10 for the later steps, because flux migrate and flux install use the API versions and manifests built into the binary.

Step 2: find objects that use object identity

List every object the affected controllers reconcile that names a ServiceAccount. This command prints the kind, the namespace and name, and the provider.

kubectl get gitrepositories,ocirepositories,buckets,imagerepositories,providers -A -o json \
  | jq -r '.items[] | select(.spec.serviceAccountName != null)
      | "\(.kind) \(.metadata.namespace)/\(.metadata.name) \(.spec.provider // .spec.type)"'

Rows with aws, azure or gcp use object identity and need the token permission. An OCIRepository with the generic provider and a ServiceAccount usually only reads image pull secrets from it, which needs no token. If the command prints nothing, skip to step 4.

Step 3: grant the token permission before you upgrade

The Flux object identity docs publish the ClusterRole to add. Commit it next to your Flux components, for example as clusters/my-cluster/flux-system/object-identity-rbac.yaml, and list it in the resources of that directory's kustomization.yaml.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: flux-object-identity
rules:
  - apiGroups:
      - ""
    resources:
      - serviceaccounts/token
    verbs:
      - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: flux-object-identity
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flux-object-identity
subjects:
  - kind: ServiceAccount
    name: source-controller
    namespace: flux-system
  - kind: ServiceAccount
    name: kustomize-controller
    namespace: flux-system
  - kind: ServiceAccount
    name: helm-controller
    namespace: flux-system
  - kind: ServiceAccount
    name: notification-controller
    namespace: flux-system
  - kind: ServiceAccount
    name: image-reflector-controller
    namespace: flux-system
  - kind: ServiceAccount
    name: image-automation-controller
    namespace: flux-system

Do this in its own commit, before the controllers change. On 2.9 the binding is redundant, so applying it changes nothing. When the 2.10 RBAC removes the old grant, this one is already in place and no source misses a beat. Once Flux has applied it, confirm the permission for one of the namespaces you found in step 2.

kubectl auth can-i create serviceaccounts --subresource=token \
  --as=system:serviceaccount:flux-system:source-controller -n apps

The answer should be yes. Granting token creation cluster-wide is a real privilege, since a controller holding it can mint tokens for any ServiceAccount. If you only use object identity in a few namespaces, bind the ClusterRole with RoleBindings in those namespaces instead.

Step 4: keep BadgerDB if a rescan would hurt

If you scan many repositories on a rate-limited registry, you can keep the old backend for now and plan the switch for a quieter day. Patch the image-reflector-controller Deployment in the same kustomization.yaml that holds gotk-components.yaml.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - gotk-components.yaml
  - gotk-sync.yaml
patches:
  - target:
      kind: Deployment
      name: image-reflector-controller
    patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --feature-gates=FluxStorage=false

The gate is opt-out from image-reflector-controller v1.3, which means it will eventually go away. Treat this as a postponement, not a decision.

Step 5: upgrade the controllers

Regenerate the components manifest with the 2.10 CLI and push it. Include the image controllers if you installed them, or flux install will leave them out and Flux will prune them.

git clone https://github.com/my-org/fleet-infra
cd fleet-infra
flux install \
  --components-extra image-reflector-controller,image-automation-controller \
  --export > ./clusters/my-cluster/flux-system/gotk-components.yaml
git add -A && git commit -m "Update Flux to $(flux -v)"
git push
flux reconcile ks flux-system --with-source

If you prefer the CLI path, rerun flux bootstrap with the same arguments you used the first time. It updates the same file in Git. Either way, flux check should report every controller healthy at its new version.

flux check

Step 6: migrate stored objects and Git manifests

The cluster now serves notification v1, but existing Alerts and Providers in etcd were written as v1beta3. flux migrate without -f rewrites every Flux object to its storage version and then sets each CRD's storedVersions to that single version. It needs cluster-admin.

flux migrate
kubectl get crd alerts.notification.toolkit.fluxcd.io -o jsonpath='{.status.storedVersions}'

Next, update the manifests in Git so they stop applying a deprecated API. With -f, flux migrate rewrites files in place. With --version=2.10 it moves Alert and Provider to v1, removes the fields v1 doesn't have and warns about each one with a hint about its replacement. Run it with --dry-run first and read the warnings.

cd fleet-infra
flux migrate -f . --version=2.10 --dry-run
flux migrate -f . --version=2.10

The warnings are your to-do list. A removed summary should move under eventMetadata, and a removed proxy should become a Secret referenced by proxySecretRef. Here is a migrated Alert that keeps its summary.

apiVersion: notification.toolkit.fluxcd.io/v1
kind: Alert
metadata:
  name: on-call
  namespace: flux-system
spec:
  providerRef:
    name: slack
  eventSeverity: error
  eventSources:
    - kind: Kustomization
      name: "*"
  eventMetadata:
    summary: "Production cluster"

Commit, push and reconcile. Then trigger a failure in a test namespace and check that the alert still arrives with its summary.

What is worth trying after the upgrade

HelmRelease with an OCI chart template

A HelmRelease could already point at an OCIRepository through spec.chartRef, but you had to declare that OCIRepository yourself. In 2.10, spec.chart.kind accepts OCIRepository. helm-controller creates and manages the source in the release namespace, and the chart version and values change in one commit.

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: podinfo
  namespace: default
spec:
  interval: 10m
  chart:
    kind: OCIRepository
    spec:
      interval: 10m
      url: oci://ghcr.io/stefanprodan/charts/podinfo
      ref:
        semver: ">= 6.0.0"
  values:
    replicaCount: 2

That atomic change is the point. With chartRef, bumping the chart and adapting the values happen in two objects, and an upgrade can run with new chart and old values in between.

Batched image updates

When CI pushes several images at once, image automation used to write one commit per update. The new spec.policyChangeDelay on ImageUpdateAutomation waits after the first ImagePolicy change, so updates that land within the delay go into one commit. The delay starts at the first change and later changes don't extend it.

apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 30m
  policyChangeDelay: 2m
  sourceRef:
    kind: GitRepository
    name: flux-system
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
      messageTemplate: |
        Automated image update

        {{ .Changed.Summary }}
    push:
      branch: main
  update:
    path: ./clusters/my-cluster
    strategy: Setters

The commit template can now use .Changed.Summary, which writes one line per image update. If pushes get rejected because people commit to the same branch, the opt-in GitPushRetryOnFailure feature gate on image-automation-controller retries on top of the latest remote state.

Unsticking field ownership

kustomize-controller has a new --override-manager-before-dry-run flag. It takes field ownership back from the named managers before the server-side apply dry run. That fixes objects that a stale or foreign field manager has wedged, which used to need a hand edit of managedFields.

The order matters

Grant the token permission before the controllers change, so object identity never loses access. Upgrade the controllers before you migrate Git, because 2.9 CRDs don't serve notification v1 and kustomize-controller would fail to apply your new manifests. Run flux migrate on the cluster after the upgrade so etcd no longer holds v1beta3 objects when a later release stops serving that version.

Test yourself

Ready to test your Flux CD knowledge? Try our Flux CD quiz!