When a HelmRelease uses impersonation spec.serviceAccountName, deleting the HelmRelease triggers a ServiceAccount existence check here
|
if err = r.Client.Get(ctx, types.NamespacedName{ |
|
Namespace: obj.GetNamespace(), |
|
Name: serviceAccount, |
|
}, &corev1.ServiceAccount{}); err != nil { |
This call only needs the get verb, but because controller-runtime's cache handles the request, it creates a cluster-wide list+watch informer for ServiceAccount. This unexpectedly requires list and watch RBAC on serviceaccounts.
With least-privilege RBAC, the delete fails with repeating errors and the uninstall never completes:
{"level":"error","ts":"2026-09-10T17:14:32.785Z","logger":"controller-runtime.cache.UnhandledError","msg":"Failed to watch","reflector":"k8s.io/client-go@v0.36.4/tools/cache/reflector.go:343","type":"*v1.ServiceAccount","error":"failed to list *v1.ServiceAccount: serviceaccounts is forbidden: User \"system:serviceaccount:flux-system:restricted\" cannot list resource \"serviceaccounts\" in API group \"\" at the cluster scope"}
Here are manual reproduction steps
kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.9.5/install.yaml
# Run helm-controller with RBAC that lacks list and watch on serviceaccounts
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: restricted
namespace: flux-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: restricted
rules:
- apiGroups: [helm.toolkit.fluxcd.io]
resources: ['*']
verbs: ['*']
- apiGroups: [source.toolkit.fluxcd.io]
resources: ['*']
verbs: ['*']
- apiGroups: ['']
resources: [secrets, configmaps]
verbs: [get, list, watch]
- apiGroups: ['']
resources: [events]
verbs: [create, patch]
- apiGroups: [coordination.k8s.io]
resources: [leases]
verbs: ['*']
- apiGroups: ['']
resources: [serviceaccounts]
verbs: [get, impersonate] # list and watch removed
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: restricted
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: restricted
subjects:
- kind: ServiceAccount
name: restricted
namespace: flux-system
EOF
kubectl -n flux-system set serviceaccount deployment/helm-controller restricted
kubectl -n flux-system delete pod -l app=helm-controller --force
# ServiceAccount to impersonate, plus a HelmRelease that uses it
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: deployer
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: deployer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: deployer
namespace: default
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
spec:
url: https://stefanprodan.github.io/podinfo
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
spec:
serviceAccountName: deployer
interval: 5m
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
EOF
kubectl wait helmrelease/podinfo --for=condition=ready
# Delete hangs to a forbidden list request
kubectl delete helmrelease podinfo --wait=false
sleep 5
kubectl -n flux-system logs deployment/helm-controller --since=30s | grep ServiceAccount
# The error, repeating:
# {"level":"error","ts":"2026-09-10T17:14:32.785Z","logger":"controller-runtime.cache.UnhandledError","msg":"Failed to watch","reflector":"k8s.io/client-go@v0.36.4/tools/cache/reflector.go:343","type":"*v1.ServiceAccount","error":"failed to list *v1.ServiceAccount: serviceaccounts is forbidden: User \"system:serviceaccount:flux-system:restricted\" cannot list resource \"serviceaccounts\" in API group \"\" at the cluster scope"}
# Podinfo still exists
kubectl get deployment podinfo
When a
HelmReleaseuses impersonationspec.serviceAccountName, deleting the HelmRelease triggers a ServiceAccount existence check herehelm-controller/internal/controller/helmrelease_controller.go
Lines 539 to 542 in f37ac2a
This call only needs the
getverb, but because controller-runtime's cache handles the request, it creates a cluster-wide list+watch informer forServiceAccount. This unexpectedly requireslistandwatchRBAC on serviceaccounts.With least-privilege RBAC, the delete fails with repeating errors and the uninstall never completes:
{"level":"error","ts":"2026-09-10T17:14:32.785Z","logger":"controller-runtime.cache.UnhandledError","msg":"Failed to watch","reflector":"k8s.io/client-go@v0.36.4/tools/cache/reflector.go:343","type":"*v1.ServiceAccount","error":"failed to list *v1.ServiceAccount: serviceaccounts is forbidden: User \"system:serviceaccount:flux-system:restricted\" cannot list resource \"serviceaccounts\" in API group \"\" at the cluster scope"}Here are manual reproduction steps