Every cluster I’ve inherited that wasn’t set up carefully from day one had the same problem: nearly everyone had cluster-admin access, “just to be safe,” because nobody wanted to deal with permission errors mid-incident. That’s exactly backwards, and it’s also exactly what Role-Based Access Control (RBAC) exists to fix — giving every user, team, and service account only the permissions they actually need. In this guide I’ll cover the core RBAC objects, walk through creating scoped roles for real scenarios, and cover how to audit and debug permission issues.
The Four Core RBAC Objects
- Role — a set of permissions (verbs like
get,list,create,deleteon resources likepods,deployments) scoped to a single namespace. - ClusterRole — the same idea, but scoped cluster-wide (or reusable across namespaces).
- RoleBinding — grants a
Role(orClusterRole) to a user, group, or service account, within a specific namespace. - ClusterRoleBinding — grants a
ClusterRolecluster-wide, to every namespace.
The key mental model: Roles/ClusterRoles define what is allowed; Bindings define who gets it and where. A ClusterRole can still be namespace-scoped in effect if it’s attached via a RoleBinding rather than a ClusterRoleBinding — this is a common and useful pattern for reusing a permission set across multiple namespaces without duplicating YAML.
Step 1: A Read-Only Role for Developers
Let’s say developers need to view Pods, logs, and Deployments in the staging namespace, but shouldn’t be able to modify anything:
# developer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: staging
name: developer-read-only
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
kubectl apply -f developer-role.yaml
Bind it to a specific user (assuming your cluster’s authentication is set up to recognize this identity, e.g. via OIDC):
# developer-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-read-only-binding
namespace: staging
subjects:
- kind: User
name: jane@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer-read-only
apiGroup: rbac.authorization.k8s.io
kubectl apply -f developer-rolebinding.yaml
Step 2: A Role for a Group Instead of an Individual
Binding to individual users doesn’t scale well. Bind to a group instead, sourced from your identity provider:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developers-group-binding
namespace: staging
subjects:
- kind: Group
name: engineering-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer-read-only
apiGroup: rbac.authorization.k8s.io
Step 3: Service Account Permissions
Applications running inside the cluster (a CI/CD runner, an operator, an in-cluster tool) authenticate via ServiceAccount, not human user accounts. Let’s grant a CI/CD deployment tool permission to manage Deployments in a specific namespace:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: deployment-manager
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: production
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: production
roleRef:
kind: Role
name: deployment-manager
apiGroup: rbac.authorization.k8s.io
kubectl apply -f ci-deployer-sa.yaml
Generate a token for use in a pipeline (short-lived, as of newer Kubernetes versions):
kubectl create token ci-deployer -n production --duration=1h
Step 4: ClusterRole for Cross-Namespace Access
Some tools genuinely need to operate cluster-wide — a monitoring agent scraping Pods across every namespace, for example:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-reader-binding
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring
roleRef:
kind: ClusterRole
name: metrics-reader
apiGroup: rbac.authorization.k8s.io
Step 5: Using Built-In ClusterRoles
Kubernetes ships several default ClusterRoles worth knowing rather than reinventing: view (read-only across most resources), edit (read-write, excluding RBAC changes and some sensitive resources), and admin (full control within a namespace, including managing Roles/RoleBindings there). cluster-admin grants unrestricted access cluster-wide and should be handed out extremely sparingly.
kubectl get clusterrole view -o yaml
Reusing these for a namespace-scoped binding is often better than writing your own from scratch:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: qa-team-edit
namespace: qa
subjects:
- kind: Group
name: qa-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
Step 6: Auditing and Debugging Permissions
The single most useful RBAC debugging command:
kubectl auth can-i delete deployments --namespace=production --as=jane@example.com
no
For a service account:
kubectl auth can-i get pods --namespace=production --as=system:serviceaccount:production:ci-deployer
yes
To see everything a subject can do (helpful for audits):
kubectl auth can-i --list --as=jane@example.com --namespace=staging
To see who can perform a specific action, tools like rbac-lookup or kubectl-who-can (a krew plugin) are worth installing:
kubectl who-can delete secrets -n production
Step 7: Least-Privilege Patterns Worth Adopting
- Scope by namespace whenever possible. Reach for
ClusterRole/ClusterRoleBindingonly when access genuinely needs to span namespaces. - Avoid wildcard verbs and resources (
verbs: ["*"],resources: ["*"]) except for genuinely trusted cluster-admin-level identities. - Separate human and machine identities. Never share a
ServiceAccounttoken across multiple unrelated tools — if one is compromised, you want the blast radius contained. - Grant
secretsaccess very deliberately. Read access to Secrets is effectively read access to whatever credentials they contain — treatget/listonsecretsas a high-privilege grant.
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["myapp-db-credentials"]
verbs: ["get"]
Scoping by resourceNames like this limits access to one named Secret instead of every Secret in the namespace — worth doing whenever the tool doesn’t genuinely need broad access.
Common Mistakes
- Binding
cluster-adminto aServiceAccount“temporarily” during setup and forgetting to revoke it. - Confusing
Role/RoleBinding(namespaced) withClusterRole/ClusterRoleBinding(cluster-wide) — aClusterRoleBindingreferencing a namespace-appropriateClusterRolewill grant that access everywhere, not just the namespace you had in mind. - Not testing permissions with
kubectl auth can-ibefore shipping a change, and discovering the gap only when something breaks in production. - Granting broad
secretsaccess to tools that only need a single specific Secret.
Best Practices
- Treat RBAC changes with the same review rigor as application code — they’re security-critical and easy to get subtly wrong.
- Regularly audit
ClusterRoleBindingsfor anything overly broad; akubectl get clusterrolebindings -o yamlreview every quarter catches drift. - Use groups from your identity provider rather than individual user bindings, so access changes when someone joins or leaves a team without manual RBAC edits.
- Combine RBAC with Pod Security Standards/admission controllers and NetworkPolicies — RBAC governs the Kubernetes API, not what a Pod can do on the network or at the OS level.
Aggregated ClusterRoles
For platform teams building internal tooling, ClusterRole supports aggregation — combining multiple smaller ClusterRoles into one composite role automatically, based on label matching, rather than manually listing every rule in one giant object:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: custom-monitoring
labels:
rbac.example.com/aggregate-to-monitoring: "true"
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-aggregate
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.example.com/aggregate-to-monitoring: "true"
rules: []
Any ClusterRole carrying the matching label automatically gets folded into monitoring-aggregate‘s effective rule set, which the built-in view, edit, and admin ClusterRoles actually use internally — this is exactly how many operators and CRDs extend default permissions without you having to hand-edit the base roles.
Impersonation for Testing and Debugging
Beyond kubectl auth can-i, you can literally act as another identity (if you yourself have impersonate permission) to verify real-world behavior, not just a yes/no permission check:
kubectl get pods -n production --as=jane@example.com
Error from server (Forbidden): pods is forbidden: User "jane@example.com" cannot list resource "pods" in API group "" in the namespace "production"
This is particularly useful when debugging a report of “my pipeline’s service account can’t do X” — impersonating the exact service account reproduces the precise error the automation is hitting, rather than guessing from the RBAC YAML alone.
kubectl get pods -n production --as=system:serviceaccount:production:ci-deployer
RBAC for Custom Resources
Operators and CRDs introduce their own resource types, and RBAC applies to them identically to built-in resources — a detail worth remembering when a team installs an operator and then can’t figure out why their otherwise-broad edit role doesn’t let them manage the new custom resource:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cnpg-cluster-manager
rules:
- apiGroups: ["postgresql.cnpg.io"]
resources: ["clusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
The built-in edit/admin ClusterRoles only cover resources known at cluster-creation time or explicitly aggregated in — CRDs installed afterward generally need their own explicit rules unless the operator’s Helm chart already wires up aggregation labels for you (many well-maintained operators do this automatically).
Namespace-Scoped Admin Delegation
A pattern I use often for multi-tenant clusters: give each team full control within their own namespace, without any cluster-wide reach at all, by combining the built-in admin ClusterRole with a namespace-scoped RoleBinding per team:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-alpha-admin
namespace: team-alpha
subjects:
- kind: Group
name: team-alpha-engineers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
Repeated per team/namespace, this gives every team meaningful self-service control (creating their own Roles and RoleBindings within their namespace, managing their own workloads) while keeping the blast radius of any single team’s mistakes or compromised credentials contained to their own namespace.
Summary
RBAC in Kubernetes comes down to four objects working together: Role/ClusterRole define permissions, RoleBinding/ClusterRoleBinding grant them to specific subjects, scoped to a namespace or the whole cluster. The discipline that actually matters is least privilege — scoping by namespace, avoiding wildcards, and being deliberate about secrets access — verified continuously with kubectl auth can-i rather than assumed.