Every engineer on your team having cluster-admin access is the Kubernetes equivalent of sharing a single root password. Here’s how to do access control properly — and why it matters more than you think.
Kubernetes Zero to Hero · Episode 08 ·
We’ve built a lot of capability over this series. Running workloads. Persisting data. Exposing services. Locking down network traffic. But there’s a dimension of security we haven’t addressed yet — and it’s the one that causes the most painful incidents in real clusters.
Who is allowed to do what?
In a freshly bootstrapped Kubernetes cluster, the default answer is often “everyone can do everything” — or worse, applications running inside the cluster have permissions they should never have. A compromised pod with cluster-admin access can read every Secret in every namespace, delete any workload, or exfiltrate data at will.
RBAC — Role-Based Access Control — is Kubernetes’ answer. It’s the system that controls which users, groups, and applications can perform which actions on which resources. When it’s configured well, a compromised component can only damage what it has access to. When it’s missing or misconfigured, one vulnerability becomes a full cluster compromise.
This episode covers RBAC completely — the model, the objects, the patterns, and the security practices that separate a hardened cluster from an exposed one.
The RBAC Mental Model
Before writing a single manifest, understand the three questions RBAC answers:
WHO → can do WHAT → on WHICH RESOURCES?
Subject Verbs Resources
(User, (get, list, (pods, secrets,
Group, create, deployments,
ServiceAcc.) delete...) nodes...)
Every RBAC decision in Kubernetes is the answer to: “Is subject X allowed to perform verb Y on resource Z?”
The four objects that implement this model:
Object → Scope → What it does
Role → Namespace → Defines a set of permissions within one namespace ClusterRole → Cluster-wide → Defines a set of permissions across the entire cluster
RoleBinding → Namespace → Grants a Role to a subject within one namespace
ClusterRoleBinding → Cluster-wide → Grants a ClusterRole to a subject cluster-wide
The mental shortcut: Roles define permissions. Bindings assign them to subjects. You can’t skip the binding — having a Role exists means nothing until it’s bound to someone.
Subjects: Who Gets Permissions
Three types of subjects can receive RBAC permissions:
Users — human identities. Kubernetes has no built-in user database. Users are authenticated externally — via client certificates, OIDC tokens (Google, Okta, Azure AD), or bearer tokens. The username is embedded in the certificate’s CN field or the token’s claims.
Groups — collections of users. Like users, groups are asserted by the authentication mechanism. A certificate with O=developers makes the bearer a member of the developers group.
ServiceAccounts — identities for pods and processes running inside the cluster. Unlike users, ServiceAccounts are actual Kubernetes objects. Every pod runs as a ServiceAccount. This is how your application code gets permissions to call the Kubernetes API.
# List ServiceAccounts in the default namespace
kubectl get serviceaccounts
NAME SECRETS AGE
default 0 5d
# Every namespace gets a "default" ServiceAccount automatically
# Pods that don't specify a ServiceAccount use this one
Roles: Defining What’s Allowed
A Role is a named collection of rules. Each rule specifies which API groups, resources, and verbs are permitted.
# developer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: production # this Role only works in this namespace
rules:
# Rule 1: Read pods and their logs
- apiGroups: [""] # "" = core API group (pods, services, configmaps...)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# Rule 2: Create and manage deployments
- apiGroups: ["apps"] # apps group (deployments, replicasets, statefulsets...)
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# Rule 3: Read ConfigMaps but not Secrets
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
The Complete Verb Reference
# Read-only verbs
verbs: ["get", "list", "watch"]
# Write verbs
verbs: ["create", "update", "patch", "delete", "deletecollection"]
# All verbs — use sparingly
verbs: ["*"]
# What each verb maps to:
# get → kubectl get <resource> <name>
# list → kubectl get <resource> (all resources of that type)
# watch → kubectl get <resource> --watch
# create → kubectl apply / kubectl create
# update → kubectl replace (full replace)
# patch → kubectl patch / kubectl apply (partial update)
# delete → kubectl delete
# deletecollection → kubectl delete <resource> --all
The Complete Resource Reference
# Core group ("") resources
resources: ["pods", "services", "endpoints", "configmaps", "secrets",
"serviceaccounts", "namespaces", "nodes", "persistentvolumes",
"persistentvolumeclaims", "events"]# apps group resources
resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
# batch group resources
resources: ["jobs", "cronjobs"]
# networking.k8s.io resources
resources: ["ingresses", "networkpolicies"]
# rbac.authorization.k8s.io resources
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# Sub-resources (specific actions on a resource)
resources: ["pods/exec"] # kubectl exec
resources: ["pods/log"] # kubectl logs
resources: ["pods/portforward"] # kubectl port-forward
RoleBindings: Connecting Roles to Subjects
A Role by itself grants nothing. You need a RoleBinding to attach it to a subject.
# developer-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: production # binding is namespace-scoped
subjects:
# Bind to a specific user
- kind: User
name: alice # must match the username in her certificate/token
apiGroup: rbac.authorization.k8s.io
# Bind to a group
- kind: Group
name: developers # all members of this group get the role
apiGroup: rbac.authorization.k8s.io
# Bind to a ServiceAccount
- kind: ServiceAccount
name: deploy-bot # ServiceAccount name
namespace: production # ServiceAccount's namespace
roleRef:
kind: Role
name: developer # must match the Role name
apiGroup: rbac.authorization.k8s.io
kubectl apply -f developer-role.yaml
kubectl apply -f developer-rolebinding.yaml
# Test what a user can do (impersonation — requires admin)
kubectl auth can-i get pods \
--namespace=production \
--as=alice
yes
kubectl auth can-i delete secrets \
--namespace=production \
--as=alice
no
# Check all permissions for a user in a namespace
kubectl auth can-i --list \
--namespace=production \
--as=alice
ClusterRoles: Cluster-Wide Permissions
ClusterRoles work identically to Roles but apply cluster-wide. Use them for:
- Permissions on cluster-scoped resources (nodes, PersistentVolumes, namespaces)
- Permissions that should apply across all namespaces
- Permissions for cluster infrastructure components
# read-only-clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-reader
rules:
# Read all resources in core and apps groups across the whole cluster
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["*"]
verbs: ["get", "list", "watch"]
# Read node information
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
# Bind the ClusterRole cluster-wide
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-reader-binding
subjects:
- kind: Group
name: sre-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-reader
apiGroup: rbac.authorization.k8s.io
The ClusterRole + RoleBinding Pattern
Here’s a useful technique: you can bind a ClusterRole with a RoleBinding (not a ClusterRoleBinding). This grants the ClusterRole’s permissions but only within a specific namespace. Perfect for reusing a common permission set across multiple namespaces without duplicating Role objects.
# Reuse the cluster-reader ClusterRole, but only in the staging namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: staging-reader-binding
namespace: staging # scoped to staging only
subjects:
- kind: User
name: bob
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole # referencing a ClusterRole
name: cluster-reader # but binding it to a namespace via RoleBinding
apiGroup: rbac.authorization.k8s.io
Built-In ClusterRoles
Kubernetes ships with several pre-defined ClusterRoles. Know them — you’ll use them constantly:
ClusterRole → What it grants
cluster-admin → Full control over everything. Use only for cluster admins. admin → Full control within a namespace. Can manage RBAC within scope. edit → Read/write most resources in a namespace. Cannot manage RBAC. view → Read-only on most resources. Cannot see Secrets.
# Grant namespace-level admin to a user in one namespace
kubectl create rolebinding alice-admin \
--clusterrole=admin \
--user=alice \
--namespace=production
# Grant cluster-wide read-only to a group
kubectl create clusterrolebinding sre-readonly \
--clusterrole=view \
--group=sre-team
# Inspect a built-in ClusterRole
kubectl describe clusterrole edit
⚠️ Never grant cluster-admin to application ServiceAccounts. This is the single most common RBAC mistake in production clusters — and it turns every pod running that ServiceAccount into a potential full cluster compromise. Application pods should have the minimum permissions they actually need, and nothing more.
ServiceAccounts: Identity for Your Applications
Every pod in Kubernetes runs as a ServiceAccount. If you don’t specify one, it uses the default ServiceAccount in its namespace. In most clusters, the default ServiceAccount has minimal permissions — which is the right default.
When your application needs to call the Kubernetes API (a CI/CD tool, a custom controller, an autoscaler), you create a dedicated ServiceAccount with only the permissions it needs.
# deploy-bot-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: deploy-bot
namespace: production
# deploy-bot-role.yaml — only what the bot actually needs
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deploy-bot-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"] # update images, scale replicas
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"] # monitor rollout status
# deploy-bot-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deploy-bot-binding
namespace: production
subjects:
- kind: ServiceAccount
name: deploy-bot
namespace: production
roleRef:
kind: Role
name: deploy-bot-role
apiGroup: rbac.authorization.k8s.io
# Reference the ServiceAccount in your pod/deployment
spec:
serviceAccountName: deploy-bot # pod runs as this identity
containers:
- name: deploy-bot
image: my-deploy-bot:1.0
kubectl apply -f deploy-bot-sa.yaml
kubectl apply -f deploy-bot-role.yaml
kubectl apply -f deploy-bot-binding.yaml
# Verify what the ServiceAccount can do
kubectl auth can-i update deployments \
--namespace=production \
--as=system:serviceaccount:production:deploy-bot
yes
kubectl auth can-i delete secrets \
--namespace=production \
--as=system:serviceaccount:production:deploy-bot
no
Disabling the Automatic Token Mount
By default, Kubernetes automatically mounts a ServiceAccount token into every pod — even if the pod never needs to call the Kubernetes API. This is an unnecessary attack surface.
# Disable auto-mount for a ServiceAccount that doesn't need API access
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: production
automountServiceAccountToken: false # no token mounted into pods
# Or disable per-pod
spec:
automountServiceAccountToken: false
containers:
- name: my-app
image: my-app:1.0
💡 Disable automountServiceAccountToken on every ServiceAccount and pod that doesn't need Kubernetes API access. Most application pods never call the Kubernetes API. Mounting a token into them for no reason is a free credential handout to anyone who compromises that pod.
A Complete RBAC Setup for a Real Team
Let’s design RBAC for a realistic engineering team with three groups and the principle of least privilege throughout.
Team structure:
- platform-admins: Manage the cluster infrastructure
- developers: Deploy and manage apps in their namespace
- read-only-users: View resources for debugging/monitoring
# 1. Platform admins — cluster-admin (sparingly, only to named individuals)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-admin-binding
subjects:
- kind: User
name: carol
apiGroup: rbac.authorization.k8s.io
- kind: User
name: david
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
# 2. Developers — full edit access in their namespace only
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-edit-binding
namespace: production
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
---
# 3. Custom role for developers — add exec access (not in built-in "edit")
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-exec
namespace: production
rules:
- apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-exec-binding
namespace: production
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-exec
apiGroup: rbac.authorization.k8s.io
---
# 4. Read-only users — view access cluster-wide (no Secrets)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: readonly-viewer-binding
subjects:
- kind: Group
name: readonly-users
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
Pod Security: Beyond RBAC
RBAC controls who can call the Kubernetes API. Pod Security controls what containers are allowed to do at the OS level. Both are necessary.
Pod Security Admission (Kubernetes 1.25+)
Kubernetes has three built-in security profiles enforced at the namespace level:
Profile → What it allows → Use for
privileged → Anything, including privileged containers → System namespaces only
baseline → Prevents the most dangerous capabilities → General workloads restricted → Strongly hardened, follows Pod hardening best practices → Security-sensitive workloads
# Apply the restricted profile to a namespace
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
With restricted enforced, pods that request hostNetwork, hostPID, privileged mode, or certain Linux capabilities will be rejected at admission — before they ever run.
Security Context: Hardening Individual Pods
spec:
securityContext:
runAsNonRoot: true # never run as root
runAsUser: 1000 # specific non-root UID
runAsGroup: 3000
fsGroup: 2000 # filesystem group for volume mounts
seccompProfile:
type: RuntimeDefault # apply seccomp filtering
containers:
- name: app
image: my-app:1.0
securityContext:
allowPrivilegeEscalation: false # cannot gain more privileges than parent
readOnlyRootFilesystem: true # container filesystem is read-only
capabilities:
drop:
- ALL # drop ALL Linux capabilities
add:
- NET_BIND_SERVICE # re-add only what's needed (bind port < 1024)
💡 readOnlyRootFilesystem: true is one of the highest-value security settings you can add. It means a compromised container cannot write malware, install tools, or modify configuration on its own filesystem. If your app needs to write to disk, use explicit volume mounts for those specific paths.
RBAC Audit: Finding What’s Too Permissive
# Who has cluster-admin?
kubectl get clusterrolebindings \
-o custom-columns='NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects' \
| grep cluster-admin
# What can the default ServiceAccount do?
kubectl auth can-i --list \
--as=system:serviceaccount:default:default
# What can the default SA do in kube-system?
kubectl auth can-i --list \
--as=system:serviceaccount:kube-system:default
# Find all RoleBindings in a namespace
kubectl get rolebindings -n production -o wide
# Find all ClusterRoleBindings
kubectl get clusterrolebindings -o wide
# Check a specific user's permissions
kubectl auth can-i --list --as=alice --namespace=production
# rakkess — a great open-source tool for RBAC visualisation
kubectl krew install access-matrix
kubectl access-matrix --namespace production
Debugging RBAC
The RBAC denial message is deliberately vague for security reasons, but there’s enough to work with:
# The error you'll see when RBAC denies a request
Error from server (Forbidden): pods is forbidden:
User "alice" cannot list resource "pods" in API group ""
in the namespace "production"
# Immediately tells you:
# Subject: alice
# Verb: list
# Resource: pods
# API group: "" (core)
# Namespace: production
# Debug by checking what she CAN do
kubectl auth can-i --list --as=alice --namespace=production
# Check if a binding exists
kubectl get rolebindings -n production -o wide | grep alice
kubectl get clusterrolebindings -o wide | grep alice
# Describe the Role to verify the rules
kubectl describe role developer -n production
# Is the subject name an exact match? (case-sensitive, whitespace matters)
kubectl get rolebinding developer-binding -n production -o yaml \
| grep -A5 subjects
⚠️ RBAC subject names are case-sensitive and must exactly match what the authentication system provides. A certificate with CN=Alice will not match a RoleBinding for alice. This is the #1 RBAC debugging issue — always check the exact subject name by looking at what the auth system actually asserts.
Hands-On Lab
# 1. Create a namespace and a ServiceAccount
kubectl create namespace rbac-lab
kubectl create serviceaccount restricted-sa -n rbac-lab
# 2. Create a Role with limited permissions
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: rbac-lab
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
EOF
# 3. Bind the Role to the ServiceAccount
kubectl create rolebinding pod-reader-binding \
--role=pod-reader \
--serviceaccount=rbac-lab:restricted-sa \
--namespace=rbac-lab
# 4. Test permissions — what can it do?
kubectl auth can-i get pods \
--as=system:serviceaccount:rbac-lab:restricted-sa \
--namespace=rbac-lab
# yes
kubectl auth can-i delete pods \
--as=system:serviceaccount:rbac-lab:restricted-sa \
--namespace=rbac-lab
# no
kubectl auth can-i get secrets \
--as=system:serviceaccount:rbac-lab:restricted-sa \
--namespace=rbac-lab
# no
# 5. Test the --list flag to see all permissions at once
kubectl auth can-i --list \
--as=system:serviceaccount:rbac-lab:restricted-sa \
--namespace=rbac-lab
# 6. Deploy a pod running as this ServiceAccount
kubectl run test-pod \
--image=nginx:1.25 \
--serviceaccount=restricted-sa \
--namespace=rbac-lab
# 7. Exec into the pod and try to use kubectl
kubectl exec -it test-pod -n rbac-lab -- sh
# Inside the pod:
# wget -qO- https://storage.googleapis.com/kubernetes-release/release/stable.txt # get kubectl version
# The pod's ServiceAccount token limits what it can call
# 8. Apply restricted Pod Security to the namespace
kubectl label namespace rbac-lab \
pod-security.kubernetes.io/enforce=restricted
# 9. Try to deploy a privileged pod — should be rejected
kubectl run privileged-test \
--image=nginx:1.25 \
--namespace=rbac-lab \
--overrides='{"spec":{"containers":[{"name":"privileged-test","image":"nginx:1.25","securityContext":{"privileged":true}}]}}'
# Error: pods "privileged-test" is forbidden: violates PodSecurity "restricted"
# 10. Clean up
kubectl delete namespace rbac-lab
Steps 4–5 are the most instructive. Running kubectl auth can-i --list and seeing exactly what a ServiceAccount can and can't do makes the permission model completely tangible — not abstract YAML on a page.
The RBAC Security Checklist
Before going to production:
□ No application ServiceAccount has cluster-admin
□ automountServiceAccountToken: false on all pods that don't need API access
□ cluster-admin bound only to named human admins, never to groups or SAs
□ Default ServiceAccount has no additional permissions beyond the default
□ Developers have edit (not admin) in their namespaces — they shouldn't manage RBAC
□ Production namespace has pod-security.kubernetes.io/enforce=restricted
□ All containers run with readOnlyRootFilesystem: true where possible
□ All containers drop ALL capabilities and re-add only what's needed
□ runAsNonRoot: true on all pods
□ allowPrivilegeEscalation: false on all containers
□ RBAC permissions reviewed and pruned quarterly
□ kubectl auth can-i --list run for every SA with non-trivial permissions
What’s Next
In Episode 09, we cover Monitoring with Prometheus and Grafana — because a cluster you can’t observe is a cluster you can’t trust. We’ll set up the full observability stack, instrument your workloads, build dashboards, and configure alerting so you know about problems before your users do.
Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 Pods, Deployments & ReplicaSets
EP 04 Services & Networking
EP 05 ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
Bonus Setting Up a Cluster with kubeadm on EC2
Bonus Network Policy Deep Dive
EP 07 Ingress Controllers & Load Balancing
EP 08 ← You are here RBAC & Security Fundamentals
EP 09 Monitoring with Prometheus & Grafana
EP 10 Production Hardening & Helm Charts
Before the Next Article
Three questions to test your understanding:
- What is the difference between a Role and a ClusterRole? When would you use a ClusterRole with a RoleBinding instead of a ClusterRoleBinding?
- Your application pod needs to list ConfigMaps in its own namespace. Write the minimum Role and RoleBinding to grant this — and only this.
- A developer reports they’re getting Forbidden errors on kubectl logs. What three things do you check, and what's the most likely fix?
If you can answer all three without looking anything up — you own this episode.
Most security incidents in Kubernetes clusters aren’t exploits of zero-day vulnerabilities. They’re the result of overly permissive RBAC, default ServiceAccounts with broad access, and containers running as root when they didn’t need to. You now know how to close all three doors. See you in Episode 09.