Drop operator privileges by shipping well-known component ServiceAccounts statically
Summary
The operator currently runs under a "super" ServiceAccount that holds the union of every permission used by every component it deploys, plus full management of rbac.authorization.k8s.io resources. This proposes refactoring so that the component ServiceAccounts and their RBAC are deployed statically alongside the operator (Helm / Kustomize / OLM), letting us prune the operator's own role down to only what its reconcilers actually call.
Background: why the operator role is so broad
This is a consequence of Kubernetes RBAC privilege-escalation prevention, not an oversight. The operator server-side-applies the full RBAC stack (ServiceAccount + Role + ClusterRole + bindings) for each component at runtime, from manifests embedded in pkg/resources/cluster/<component>/.
To create a Role/ClusterRole with permission set P, the API server requires the caller to already hold every permission in P (or hold the escalate verb); to create a binding, it must hold the role's permissions (or bind). So config/rbac/role.yaml is forced to be the union of all component permissions plus create/update/delete on roles/clusterroles/rolebindings/clusterrolebindings.
That's why the operator holds permissions it never uses itself — only to be allowed to grant them:
rbac.authorization.k8s.io roles/clusterroles/rolebindings/clusterrolebindings (all verbs)
internal.linstor.linbit.com/* (the LINSTOR controller's database)
pods/eviction (ha-controller)
- snapshot + groupsnapshot APIs,
volumeattachments, csinodes, csistoragecapacities (CSI sidecars)
endpointslices (nfs-server)
securitycontextconstraints use (component pods)
There is already a precedent for the target pattern: the gencert job ships its own fixed SA + narrow RBAC statically (config/gencert/rbac.yaml), and all component SA names are already fixed/well-known.
Components and their ServiceAccounts
| Component |
SA name |
Source dir |
| linstor-controller |
linstor-controller |
pkg/resources/cluster/controller/ |
| csi-controller |
linstor-csi-controller |
pkg/resources/cluster/csi-controller/ |
| csi-node |
linstor-csi-node |
pkg/resources/cluster/csi-node/ |
| ha-controller |
ha-controller |
pkg/resources/cluster/ha-controller/rbac.yaml |
| affinity-controller |
linstor-affinity-controller |
pkg/resources/cluster/affinity-controller/ |
| nfs-server |
linstor-csi-nfs-server |
pkg/resources/cluster/nfs-server/ |
| satellite |
satellite |
pkg/resources/cluster/satellite-common/ |
Goal
- Operator no longer holds any
rbac.authorization.k8s.io permissions and no longer creates/owns component RBAC.
- Component SAs/Roles/bindings are created by the installer with fixed, minimal, auditable permissions.
- Operator role is reduced to the kinds its reconcilers directly apply or read.
Proposed approach
BEFORE: installer -> [operator SA = superset] -> operator creates all component SA+RBAC at runtime
AFTER: installer -> operator SA (minimal) + all component SAs/Roles/bindings (fixed, static)
operator only references SAs by name; never touches rbac.authorization.k8s.io
Key enabling decision: which optional components (NFS / HA / affinity / external-controller) get deployed is driven by the LinstorCluster CR at runtime, which the installer cannot know in advance. Resolve this by shipping all component SAs/Roles/bindings statically and unconditionally. An unused ServiceAccount + Role grants nothing (nothing runs under it), so pre-creating them all is inert and safe. This is what makes static RBAC compatible with CR-driven component selection.
Tasks
1. Lift component RBAC into install artifacts (keep 3 surfaces in sync)
2. Stop the operator creating/owning component RBAC
3. Drop the operator's privileges
4. Migration / ownership handover
5. Docs & validation
Open decisions
- Backward compat: adoption migration (recommended) vs. documented uninstall/reinstall on upgrade.
- Component toggles: ship all component SAs unconditionally (recommended) vs. gate each behind Helm values.
- Rollout order: all three install surfaces at once vs. Helm first, then Kustomize/OLM.
Acceptance criteria
Affected files (reference)
internal/controller/linstorcluster_controller.go (rbac markers, prune list)
internal/controller/linstorsatellite_controller.go (prune list)
pkg/resources/cluster/*/ (component RBAC + kustomization.yaml), pkg/resources/cluster/resources.go (embed sets)
config/rbac/role.yaml, config/default/kustomization.yaml, new config/component-rbac/
charts/piraeus/templates/rbac.yaml + new component RBAC templates, tools/copy-rbac-config-to-chart.sh
bundle/manifests/piraeus-operator.clusterserviceversion.yaml
Drop operator privileges by shipping well-known component ServiceAccounts statically
Summary
The operator currently runs under a "super" ServiceAccount that holds the union of every permission used by every component it deploys, plus full management of
rbac.authorization.k8s.ioresources. This proposes refactoring so that the component ServiceAccounts and their RBAC are deployed statically alongside the operator (Helm / Kustomize / OLM), letting us prune the operator's own role down to only what its reconcilers actually call.Background: why the operator role is so broad
This is a consequence of Kubernetes RBAC privilege-escalation prevention, not an oversight. The operator server-side-applies the full RBAC stack (
ServiceAccount+Role+ClusterRole+ bindings) for each component at runtime, from manifests embedded inpkg/resources/cluster/<component>/.To create a
Role/ClusterRolewith permission set P, the API server requires the caller to already hold every permission in P (or hold theescalateverb); to create a binding, it must hold the role's permissions (orbind). Soconfig/rbac/role.yamlis forced to be the union of all component permissions plus create/update/delete on roles/clusterroles/rolebindings/clusterrolebindings.That's why the operator holds permissions it never uses itself — only to be allowed to grant them:
rbac.authorization.k8s.ioroles/clusterroles/rolebindings/clusterrolebindings (all verbs)internal.linstor.linbit.com/*(the LINSTOR controller's database)pods/eviction(ha-controller)volumeattachments,csinodes,csistoragecapacities(CSI sidecars)endpointslices(nfs-server)securitycontextconstraintsuse (component pods)There is already a precedent for the target pattern: the
gencertjob ships its own fixed SA + narrow RBAC statically (config/gencert/rbac.yaml), and all component SA names are already fixed/well-known.Components and their ServiceAccounts
linstor-controllerpkg/resources/cluster/controller/linstor-csi-controllerpkg/resources/cluster/csi-controller/linstor-csi-nodepkg/resources/cluster/csi-node/ha-controllerpkg/resources/cluster/ha-controller/rbac.yamllinstor-affinity-controllerpkg/resources/cluster/affinity-controller/linstor-csi-nfs-serverpkg/resources/cluster/nfs-server/satellitepkg/resources/cluster/satellite-common/Goal
rbac.authorization.k8s.iopermissions and no longer creates/owns component RBAC.Proposed approach
Key enabling decision: which optional components (NFS / HA / affinity / external-controller) get deployed is driven by the
LinstorClusterCR at runtime, which the installer cannot know in advance. Resolve this by shipping all component SAs/Roles/bindings statically and unconditionally. An unusedServiceAccount+Rolegrants nothing (nothing runs under it), so pre-creating them all is inert and safe. This is what makes static RBAC compatible with CR-driven component selection.Tasks
1. Lift component RBAC into install artifacts (keep 3 surfaces in sync)
charts/piraeus/templates/for each component's SA/Role/ClusterRole/bindings (namespace.Release.Namespace, standard chart labels), optionally gated by.Values.<component>.rbac.create(defaulttrue).config/component-rbac/base and wire it intoconfig/default/kustomization.yaml.bundle/manifests/piraeus-operator.clusterserviceversion.yamlunderspec.install.spec.permissions/clusterPermissions(mirroring the existinggencertsplit).2. Stop the operator creating/owning component RBAC
kustomization.yamlresources:list and from the//go:embedsets inpkg/resources/cluster/resources.go(and satellite equivalent).rbacv1.Role/ClusterRole/RoleBinding/ClusterRoleBindingandcorev1.ServiceAccountfrom the prune lists ininternal/controller/linstorcluster_controller.go(utils.PruneResources, ~line 256) and the satellite controller — otherwise the operator deletes the now-static objects.serviceAccountName: linstor-controller, etc.); no change expected since the SAs now come from the installer in the same namespace.3. Drop the operator's privileges
//+kubebuilder:rbacmarkers ininternal/controller/linstorcluster_controller.go(therbac.authorization.k8s.iomarker is the headline removal).piraeus.ioCRs/status/finalizers;appsdeployments+daemonsets; coreconfigmaps/services/secrets+nodes/podsread;apiextensionsCRDs (audit);cert-manager.iocertificates;cluster.x-k8s.iomachines;storage.k8s.iocsidrivers only;events; leader-election leases.rbac.authorization.k8s.io;serviceaccountscreate/delete;internal.linstor.linbit.com/*;pods/eviction; snapshot + groupsnapshot APIs;volumeattachments/csinodes/csistoragecapacities/storageclasses/volumeattributesclasses;endpointslices; SCCuse; PV/PVC write.make manifests, thentools/copy-rbac-config-to-chart.sh, to regenerateconfig/rbac/role.yamlandcharts/piraeus/templates/rbac.yaml.clusterPermissionsto match the regenerated operator role.4. Migration / ownership handover
meta.helm.sh/release-name,meta.helm.sh/release-namespace, labelapp.kubernetes.io/managed-by: Helm) sohelm upgradeadopts the existing objects by name instead of erroring.pre-upgradehook Job, or operator startup logic) to stripownerReferencesfrom the pre-existing component RBAC objects, decoupling their lifecycle from theLinstorClusterCR (prevents GC from deleting them if the CR is later removed). Fresh installs need none of this.5. Docs & validation
docs/(RBAC/security + upgrade notes): document the well-known SAs and the reduced operator role.rbac.authorization.k8s.iorules, so a stray future+kubebuilder:rbacmarker can't silently re-bloat it.Open decisions
Acceptance criteria
config/rbac/role.yamlcontains norbac.authorization.k8s.iorules and none of the "grant-only" permissions listed above.Affected files (reference)
internal/controller/linstorcluster_controller.go(rbac markers, prune list)internal/controller/linstorsatellite_controller.go(prune list)pkg/resources/cluster/*/(component RBAC +kustomization.yaml),pkg/resources/cluster/resources.go(embed sets)config/rbac/role.yaml,config/default/kustomization.yaml, newconfig/component-rbac/charts/piraeus/templates/rbac.yaml+ new component RBAC templates,tools/copy-rbac-config-to-chart.shbundle/manifests/piraeus-operator.clusterserviceversion.yaml