Trim operator RBAC to least privilege (workstream E) - #84
Merged
Conversation
Reduce the operator's runtime blast radius by granting only the permissions the code actually exercises. configmaps: the operator reads exactly one ConfigMap, the emergency kill switch (ballast-kill-switch) in its own namespace. Move the grant off the cluster-wide ClusterRole onto a namespaced Role in the release namespace, so the process can no longer read every ConfigMap in the cluster. The kill-switch informer is scoped in code to that single object via the manager cache (namespace + metadata.name field selector), and the get verb is pinned with resourceNames (list/watch can't be name-restricted via RBAC). Drop unused verbs from the ClusterRole, all verified against the code: - pods: no update (pods are only patched; in-place resize uses the pods/resize subresource) - nodes: no get (the kubelet source only lists nodes) - events: no patch (events are only created, uniquely named) - workloadprofiles: no update (create/patch/delete only) - workloadprofiles/status: patch only (status is written via Status().Patch and read through the parent object) No functional change; the operator exercises none of the removed permissions. The pod securityContext is already PSS-restricted compliant (runAsNonRoot, seccompProfile RuntimeDefault, readOnlyRootFilesystem, allowPrivilegeEscalation false, drop ALL caps), so it is unchanged.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Docker Image Builtdocker pull ghcr.io/tight-line/ballast:pr-84-9d1aa6cHelm (values override)image:
repository: ghcr.io/tight-line/ballast
tag: "pr-84-9d1aa6c"Image expires ~15 days after PR closes. |
config/rbac/role.yaml (manager-role) and config/rbac/role_binding.yaml (manager-rolebinding) were a stale controller-gen stub granting only pods get,list,watch, wildly out of sync with the real operator policy. There are no +kubebuilder:rbac markers anywhere in the code, so controller-gen never regenerated the file (a real delete + `make manifests` leaves it gone); it was a hand-frozen artifact that nothing generated and nothing deployed. Ballast installs exclusively via the Helm chart, whose hand-maintained ClusterRole (charts/ballast/templates/clusterrole.yaml) is the true, deployed policy and now the sole source of truth. Delete both files, drop their entries from config/rbac/kustomization.yaml, and document the Helm ClusterRole as authoritative there. Nothing runs `kustomize build`, and no other manifest references manager-role / manager-rolebinding, so no dangling references remain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Workstream E — runtime blast-radius reduction. Grants the operator only the Kubernetes permissions its code actually exercises, and removes an orphaned RBAC artifact that had drifted from the real policy. Every removed permission was verified as unused against the source (see below); no functional change.
The main win: scope
configmapsto the operator's own namespaceThe operator reads exactly one ConfigMap: the emergency kill switch
ballast-kill-switch, in its own namespace (internal/killswitch/killswitch.go). But theClusterRolegrantedconfigmaps get,list,watchcluster-wide, because the manager cache only namespace-scoped Pods, so the ConfigMap informer watched every namespace.This PR:
cmd/ballastd/main.go) tooperatorNamespace+ ametadata.namefield selector, so the process only ever lists/watches the single kill-switch object.configmapsgrant off the cluster-wideClusterRoleonto a namespacedRolein the release namespace.getis pinned withresourceNames: [ballast-kill-switch];list/watchstay namespace-scoped (RBAC can't name-restrict those, but the code-side cache scoping means they only ever touch the one object).Net effect: the operator can no longer read ConfigMaps anywhere outside its own namespace, closing a common read-surface (CA bundles, app config, occasionally-misplaced secrets).
Verb trims on the
ClusterRoleAll confirmed unused by grepping the codebase for the corresponding client calls:
podsPatched; in-place resize uses thepods/resizesubresource. Noclient.Updateon pods anywhere.nodesLists nodes; per-node data comes vianodes/proxy.eventsCreated (no aggregation/patch path).workloadprofilesworkloadprofiles/statusStatus().Patchand read through the parent object, never as a/statusGET.Removed the orphaned
manager-roleRBAC stubconfig/rbac/role.yaml(the controller-genmanager-role) and its bindingconfig/rbac/role_binding.yamlgranted onlypods get,list,watch— wildly out of sync with the real operator policy. There are no+kubebuilder:rbacmarkers anywhere in the code, so controller-gen never regenerated it (verified: a real delete followed bymake manifestsleaves it gone). It was a hand-frozen artifact that nothing generates and nothing deploys — Ballast installs exclusively via the Helm chart.This PR deletes both files, drops their entries from
config/rbac/kustomization.yaml, and documents the HelmClusterRole(charts/ballast/templates/clusterrole.yaml) as the sole source of truth for operator permissions. Nothing runskustomize build, and no other manifest referencesmanager-role/manager-rolebinding, so no dangling references remain.securityContext — already hardened, unchanged
The deployment is already Pod Security Standards restricted-compliant: pod-level
runAsNonRoot+seccompProfile: RuntimeDefault; container-levelreadOnlyRootFilesystem,allowPrivilegeEscalation: false,capabilities.drop: [ALL]. No change needed.Validation
make lint— clean (0 issues)make test-coverage-check— passedhelm lint charts/ballast— passed; RBAC renders correctly withleaderElectbothtrueandfalsemake manifests— clean tree (does not regenerate the removed stub)go build ./cmd/ballastd— clean