Summary
The operator's own ClusterRole (<release>-manager-role-cluster, rendered when watchNamespaces is
empty / cluster-wide, which is the default) only grants get/list on openclawclusterdefaults.
It's missing permissions for openclawinstances, openclawselfconfigs, and every child resource
the controller creates — so a fresh install cannot reconcile any OpenClawInstance at all out of
the box.
Environment
- Chart:
oci://ghcr.io/paperclipinc/charts/openclaw-operator, version 0.38.3 (appVersion 0.38.3)
- Install method:
helm install openclaw-operator oci://ghcr.io/paperclipinc/charts/openclaw-operator --set webhook.enabled=true --set metrics.serviceMonitor.enabled=true
- Kubernetes: v1.35.5+k3s1 (k3s/k3d)
Steps to reproduce
helm install the chart with default watchNamespaces: [] (cluster-wide).
- Apply a minimal
OpenClawInstance.
kubectl logs deploy/openclaw-operator -n openclaw-operator-system
Actual behavior
Immediately on startup, the controller's informers fail to even list the CRD it's supposed to
manage:
failed to list *v1alpha1.OpenClawInstance: openclawinstances.openclaw.rocks is forbidden:
User "system:serviceaccount:openclaw-operator-system:openclaw-operator" cannot list resource
"openclawinstances" in API group "openclaw.rocks" at the cluster scope
Once that's worked around, reconciliation proceeds but fails again, one resource type at a time,
for every child resource kind: statefulsets, deployments, services, configmaps, secrets,
serviceaccounts, persistentvolumeclaims, events, jobs, cronjobs, networkpolicies,
ingresses, poddisruptionbudgets, horizontalpodautoscalers, roles, rolebindings,
httproutes.gateway.networking.k8s.io, servicemonitors.monitoring.coreos.com,
prometheusrules.monitoring.coreos.com.
Inspecting the rendered ClusterRole:
$ kubectl get clusterrole <release>-manager-role-cluster -o yaml
rules:
- apiGroups: [openclaw.rocks]
resources: [openclawclusterdefaults]
verbs: [get, list]
That's the entirety of the operator's own cluster-scoped grant. By contrast, the chart does
correctly generate <release>-aggregate-to-admin/edit/view ClusterRoles with full
openclawinstances/openclawselfconfigs permissions — but those are aggregated into the
built-in admin/edit/view roles for human users, not bound to the operator's own
ServiceAccount.
Expected behavior
The operator's own ClusterRole should include everything its reconcile loop actually touches —
presumably the full kubebuilder-marker-generated rule set is meant to land in the "manager"
ClusterRole/Role, but appears to have been dropped for the cluster-scoped rendering path.
Workaround used
Created a supplemental ClusterRole/ClusterRoleBinding granting the operator's ServiceAccount
(openclaw-operator-system:openclaw-operator) full CRUD on all of the above resource types, kept
outside Helm's ownership so it survives helm upgrade.
Related: watchNamespaces scoping is also broken
Separately, setting watchNamespaces: [my-namespace] (to render namespaced Role/RoleBinding
instead of cluster-scoped) causes the operator to watch its own namespace
(openclaw-operator-system) for child resources instead of the target namespace:
failed to list *v1.Deployment: deployments.apps is forbidden: User
"...openclaw-operator" cannot list resource "deployments" in API group "apps"
in the namespace "openclaw-operator-system"
Reverting to the cluster-wide default (watchNamespaces: []) was the only way to get reconciliation
working, even though single-namespace scoping is otherwise a reasonable thing to want for tighter
RBAC.
Summary
The operator's own
ClusterRole(<release>-manager-role-cluster, rendered whenwatchNamespacesisempty / cluster-wide, which is the default) only grants
get/listonopenclawclusterdefaults.It's missing permissions for
openclawinstances,openclawselfconfigs, and every child resourcethe controller creates — so a fresh install cannot reconcile any
OpenClawInstanceat all out ofthe box.
Environment
oci://ghcr.io/paperclipinc/charts/openclaw-operator, version0.38.3(appVersion0.38.3)helm install openclaw-operator oci://ghcr.io/paperclipinc/charts/openclaw-operator --set webhook.enabled=true --set metrics.serviceMonitor.enabled=trueSteps to reproduce
helm installthe chart with defaultwatchNamespaces: [](cluster-wide).OpenClawInstance.kubectl logs deploy/openclaw-operator -n openclaw-operator-systemActual behavior
Immediately on startup, the controller's informers fail to even list the CRD it's supposed to
manage:
Once that's worked around, reconciliation proceeds but fails again, one resource type at a time,
for every child resource kind:
statefulsets,deployments,services,configmaps,secrets,serviceaccounts,persistentvolumeclaims,events,jobs,cronjobs,networkpolicies,ingresses,poddisruptionbudgets,horizontalpodautoscalers,roles,rolebindings,httproutes.gateway.networking.k8s.io,servicemonitors.monitoring.coreos.com,prometheusrules.monitoring.coreos.com.Inspecting the rendered
ClusterRole:That's the entirety of the operator's own cluster-scoped grant. By contrast, the chart does
correctly generate
<release>-aggregate-to-admin/edit/viewClusterRoles with fullopenclawinstances/openclawselfconfigspermissions — but those are aggregated into thebuilt-in
admin/edit/viewroles for human users, not bound to the operator's ownServiceAccount.
Expected behavior
The operator's own ClusterRole should include everything its reconcile loop actually touches —
presumably the full kubebuilder-marker-generated rule set is meant to land in the "manager"
ClusterRole/Role, but appears to have been dropped for the cluster-scoped rendering path.
Workaround used
Created a supplemental
ClusterRole/ClusterRoleBindinggranting the operator's ServiceAccount(
openclaw-operator-system:openclaw-operator) full CRUD on all of the above resource types, keptoutside Helm's ownership so it survives
helm upgrade.Related:
watchNamespacesscoping is also brokenSeparately, setting
watchNamespaces: [my-namespace](to render namespacedRole/RoleBindinginstead of cluster-scoped) causes the operator to watch its own namespace
(
openclaw-operator-system) for child resources instead of the target namespace:Reverting to the cluster-wide default (
watchNamespaces: []) was the only way to get reconciliationworking, even though single-namespace scoping is otherwise a reasonable thing to want for tighter
RBAC.