The Day Everything Almost Broke
A junior DevOps engineer on my team once deployed a CI/CD pipeline where the build pod had access to list, create, and delete nodes across the entire cluster. It worked fine for months — until someone accidentally triggered a cleanup script that wiped production workloads. Root cause? The ServiceAccount had cluster-admin privileges because nobody had bothered to scope it properly.
One incident. Three hours of recovery. A very uncomfortable post-mortem.
That experience hammered home something I now treat as a rule: in Kubernetes, what you don’t restrict will eventually bite you. This guide walks through setting up RBAC properly — no assumed knowledge, no hand-waving.
Why Over-Permissive Access Keeps Happening
It almost always traces back to one of three things:
- Speed over security — granting
cluster-adminto unblock someone in the moment - Unclear ownership — nobody knows what a ServiceAccount actually needs, so it gets everything
- Misconfigured defaults — the
defaultServiceAccount in a namespace quietly accumulates more permissions than intended
Kubernetes has shipped with RBAC enabled by default since v1.6. But enabled isn’t the same as configured. The API server will happily accept a RoleBinding that hands a pod full write access to secrets across all namespaces — no warning, no prompt, no guardrails.
How Kubernetes RBAC Actually Works
Four objects make up the entire RBAC system. Get these straight first and everything else clicks:
- Role — defines permissions within a single namespace
- ClusterRole — defines permissions cluster-wide, or reusably across namespaces
- RoleBinding — grants a Role (or ClusterRole) to a user, group, or ServiceAccount within one namespace
- ClusterRoleBinding — grants a ClusterRole cluster-wide, no namespace restriction
A ServiceAccount is just an identity that pods use to authenticate with the API server. By itself it can do nothing — permissions only exist once you attach a binding.
The mental model that helped me most: think of Roles as job descriptions, and Bindings as signed employment contracts.
Three Common Patterns (And What’s Wrong With Two of Them)
Pattern 1: Just Use cluster-admin
The path of least resistance. Bind the built-in cluster-admin ClusterRole to your ServiceAccount and move on with your day.
# Don't do this in production
kubectl create clusterrolebinding my-app-admin \
--clusterrole=cluster-admin \
--serviceaccount=production:my-app
Full cluster control. Create, delete, modify anything. It solves the immediate problem, but now anyone who can exec into that pod effectively owns your cluster. One compromised container and it’s game over.
Pattern 2: Use Built-in ClusterRoles
Kubernetes ships with around 50 built-in roles. The useful general-purpose ones are view, edit, and admin. Better than cluster-admin, but still broader than most workloads need.
# Better, but usually still too broad
kubectl create rolebinding my-app-viewer \
--clusterrole=view \
--serviceaccount=production:my-app \
--namespace=production
view gives read-only access to most resources. edit adds write access. These are fine for quick setups — but they bundle in resources your app probably never touches, and every unnecessary permission is a liability.
Pattern 3: Custom Roles with Least Privilege
More upfront work. Worth it every time.
You define exactly which verbs (get, list, watch, create, update, delete) apply to exactly which resources in exactly which namespace. Nothing more. This is the only approach that actually limits blast radius when something goes wrong — and something always eventually goes wrong.
Step-by-Step RBAC Setup
Let’s use a concrete example: a monitoring application that reads pod and node metrics but should never modify anything.
Step 1: Create a Dedicated Namespace and ServiceAccount
kubectl create namespace monitoring
kubectl create serviceaccount metrics-reader -n monitoring
Never use the default ServiceAccount for real workloads. A dedicated ServiceAccount makes auditing clean and permission scoping explicit — you always know exactly what’s bound to what.
Step 2: Define a Role with Minimal Permissions
Reading node metrics requires cluster-wide access, so we need a ClusterRole here rather than a namespace-scoped Role:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "nodes/metrics"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods", "nodes"]
verbs: ["get", "list"]
kubectl apply -f metrics-reader-clusterrole.yaml
Three things worth noting:
apiGroups: [""]targets the core API group — pods, nodes, services, configmaps- Only
get,list,watch— nocreate,update, ordelete - Resources listed explicitly, not wildcarded
Step 3: Bind the Role to the ServiceAccount
Node metrics need cluster-wide visibility, so a ClusterRoleBinding is appropriate here:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-reader-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-reader
subjects:
- kind: ServiceAccount
name: metrics-reader
namespace: monitoring
kubectl apply -f metrics-reader-binding.yaml
Step 4: Assign the ServiceAccount to Your Pod
apiVersion: v1
kind: Pod
metadata:
name: metrics-collector
namespace: monitoring
spec:
serviceAccountName: metrics-reader
containers:
- name: collector
image: your-metrics-image:latest
Step 5: Verify What the ServiceAccount Can Actually Do
Run this before deploying — not after wondering why something broke:
# Should return: yes
kubectl auth can-i list pods \
--as=system:serviceaccount:monitoring:metrics-reader
# Should return: no
kubectl auth can-i delete pods \
--as=system:serviceaccount:monitoring:metrics-reader
# Dump the full permission set
kubectl auth can-i --list \
--as=system:serviceaccount:monitoring:metrics-reader
If that second command returns yes, your Role is too permissive. Fix it now, not in a post-mortem.
Step 6: Disable Automounting When Not Needed
Kubernetes automounts the ServiceAccount token into every pod by default. If your pod never calls the Kubernetes API directly, that token is just an attack surface sitting on the filesystem:
apiVersion: v1
kind: ServiceAccount
metadata:
name: metrics-reader
namespace: monitoring
automountServiceAccountToken: false
Or disable it per-pod instead:
spec:
automountServiceAccountToken: false
No token on disk means no way to authenticate against the API server, even if the role bindings exist. Defense in depth.
Common Mistakes Worth Calling Out
- Wildcards in resources or verbs —
resources: ["*"]grants access to everything. That’s cluster-admin in disguise. - ClusterRoleBinding when a RoleBinding is enough — if your app only touches one namespace, scope the binding there, even if the Role itself is a ClusterRole
- Never auditing existing bindings — run
kubectl get clusterrolebindings -o wideperiodically; privilege creep is real and it accumulates quietly - Skipping the
auth can-icheck — five minutes of verification beats two hours of incident response
A Note on ServiceAccount Credentials
When setting up cluster access for human users (not just pods), you’ll generate kubeconfig files with credentials. For tokens and passwords, I use the generator at toolcraft.app/en/tools/security/password-generator — it runs entirely in the browser, so nothing touches a server. For anything security-related, client-side-only matters.
Auditing Your RBAC Configuration
Set up periodic audits. These three commands cover most of what you need:
# Find every ServiceAccount with cluster-admin
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | \
{name: .metadata.name, subjects: .subjects}'
# List all RoleBindings in a namespace
kubectl get rolebindings -n production -o wide
# Check who can perform sensitive operations
kubectl auth can-i create pods --all-namespaces --list
For larger clusters, rbac-lookup and kubectl-who-can make this considerably less painful.
The Principle in Practice
Least privilege isn’t a checkbox. It’s a habit you build into every deployment.
Each time you add a new workload, ask: what does this pod actually need to call? Start with nothing. Add permissions only when something breaks and you can clearly justify why. The monitoring example above is straightforward — real applications get messier. Operators managing CRDs, pipelines deploying across namespaces, admission webhooks reading secrets. The approach stays the same regardless.
RBAC misconfiguration shows up consistently as one of the top Kubernetes security findings in production clusters — not because the tooling is hard, but because nobody made time to do it right the first time. That post-mortem my team sat through was avoidable. Yours can be too.

