By KP | TZoneLabs | DevOps & Cloud Engineering
A Role scoped to one namespace, bound to one service account, with verbs limited to exactly what that workload does, is what turns Kubernetes RBAC into an actual security boundary instead of a checkbox. Most clusters we’ve audited get this half right: the Role exists, but the RoleBinding quietly targets a broader group than intended, or a wildcard verb from a debugging session never got removed.
This post walks through the four RBAC objects, a working least-privilege Role and RoleBinding for a CI deploy service account, a read-only ClusterRole for monitoring tools, how to verify permissions with kubectl auth can-i instead of assuming the YAML did what it says, and the mistakes that quietly widen access past what anyone intended.
The Four Objects RBAC Is Built From
Role— a set of permissions (verbs on resources) scoped to a single namespace.RoleBinding— grants a Role to a user, group, or service account, also scoped to that namespace.ClusterRole— the same kind of permission set, but not tied to a namespace. Can be bound namespace-by-namespace or cluster-wide.ClusterRoleBinding— grants a ClusterRole across the entire cluster, every namespace at once.
The mistake we see most often is reaching for a ClusterRole and ClusterRoleBinding by default because it’s one less namespace to think about, when a namespaced Role and RoleBinding would have contained the blast radius to where the workload actually runs.
A Least-Privilege Role for a CI Deploy Service Account
Say GitLab CI needs to deploy to the staging namespace: it should be able to manage Deployments and read Pods and Services there, and nothing else — no Secrets, no other namespaces, no cluster-scoped resources.
Start with a dedicated ServiceAccount instead of reusing default:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: staging
Then a Role that grants only what the deploy job actually calls:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer-role
namespace: staging
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]
And the RoleBinding that connects the two, in the same namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: staging
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: staging
roleRef:
kind: Role
name: ci-deployer-role
apiGroup: rbac.authorization.k8s.io
Nothing in this pipeline job can read a Secret, touch a different namespace, or delete a node, even if the pipeline definition itself is compromised.
A Read-Only ClusterRole for Monitoring Tools
Some workloads genuinely need cluster-wide read access — a monitoring agent scraping Pods and Nodes across every namespace is the common case. That’s a legitimate use for a ClusterRole, as long as the verbs stay read-only:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-read-only
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: monitoring-read-only-binding
subjects:
- kind: ServiceAccount
name: monitoring-agent
namespace: monitoring
roleRef:
kind: ClusterRole
name: monitoring-read-only
apiGroup: rbac.authorization.k8s.io
The distinction that matters: cluster-wide is fine when the verbs are read-only. Cluster-wide and write access on the same binding is how one compromised monitoring pod becomes a cluster-wide problem.
Verifying Permissions With kubectl auth can-i
Don’t assume the YAML granted what you think it granted. Check it directly, impersonating the service account:
kubectl auth can-i create deployments \
--namespace staging \
--as system:serviceaccount:staging:ci-deployer
yes
Then confirm the boundary holds — the same service account should not be able to touch Secrets or another namespace:
kubectl auth can-i get secrets \
--namespace staging \
--as system:serviceaccount:staging:ci-deployer
kubectl auth can-i get deployments \
--namespace production \
--as system:serviceaccount:staging:ci-deployer
no
no
If either of those comes back yes, something upstream — an aggregated ClusterRole, a stray ClusterRoleBinding, a group membership — is granting more than the Role you wrote. kubectl auth can-i tells you what’s actually enforced, not what the manifest intended.
Mistakes That Quietly Widen Access
- Wildcard verbs or resources.
verbs: ["*"]orresources: ["*"]almost always outlives the debugging session it was added for. Every wildcard we’ve found in an audit was still there months later, unnoticed. - Binding to
system:authenticatedor a broad group. It’s an easy way to unblock a permission error fast, and an easy way to grant that permission to every workload and user in the cluster, not just the one that needed it. - ClusterRoleBinding where a namespaced RoleBinding would do. If the workload only ever runs in one namespace, bind it there. A ClusterRoleBinding turns a namespace-scoped mistake into a cluster-scoped one.
- Assuming
list/watchis harmless. Read access to Secrets across a namespace is still read access to every credential stored there. ScoperesourceNamesdown to specific objects when the workload only needs one. - Never auditing existing bindings. RBAC accretes — a role added for a one-off task rarely gets removed once the task is done.
kubectl get clusterrolebindings -o wideandkubectl get rolebindings --all-namespaces -o wideare worth running on a schedule, not just when something goes wrong.
Key Lessons
-
Default to a namespaced Role and RoleBinding, not a ClusterRole.
Reach for cluster-wide scope only when the workload genuinely needs it, not because it saves a namespace field. -
Cluster-wide access should mean read-only.
A ClusterRole with write verbs turns one compromised pod into a cluster-wide incident. -
Verify with
kubectl auth can-i, don’t trust the manifest.
Aggregated ClusterRoles and stray bindings can grant more than the Role you wrote, and the YAML alone won’t show you that. -
Wildcards from a debugging session don’t remove themselves.
Treat any"*"in a Role as temporary the moment it’s added, and go back and scope it before the ticket closes. -
Audit bindings on a schedule, not just after an incident.
RBAC only accumulates permissions over time unless something actively prunes it.
Summary
| Object | Scope | Use When |
|---|---|---|
Role + RoleBinding |
Single namespace | Default choice for any workload scoped to one namespace |
ClusterRole + ClusterRoleBinding |
Cluster-wide | Genuinely cross-namespace needs, ideally read-only |
ClusterRole + RoleBinding |
One namespace, reusable role | Same permission set needed in several namespaces without duplicating the Role |
kubectl auth can-i |
Verification, not a grant | Every time a binding changes, before assuming it’s correct |
None of this needs a dedicated security team to maintain — it needs the Role and RoleBinding written for the workload in front of you, not copied from whichever example granted the most access.
Read Next
- Setting Up Kubernetes Cluster Autoscaler on EKS: The Settings That Actually Matter
- A Namespace Relabel Silently Broke a NetworkPolicy Selector, and Cross-Namespace Traffic Started Timing Out
If you’re running Kubernetes in production, follow along on LinkedIn for more guides like this one as they’re published.
Tags:
#Kubernetes #RBAC #DevOps #Security
#K8sSecurity #SRE