RBAC Deep Dive
In December 2023, a security researcher discovered that thousands of Kubernetes clusters on the public internet had exposed dashboards running with cluster-admin privileges. Attackers weren't exploiting vulnerabilities—they were using the default service account that Kubernetes automatically mounts into every pod, which often had more permissions than the application ever needed.
Your Task API doesn't need cluster-admin access. It doesn't need access to secrets across all namespaces. It needs exactly one thing: the ability to read its own ConfigMap. RBAC (Role-Based Access Control) lets you specify precisely that—and nothing more.
This lesson builds the RBAC foundation that protects your Task API from privilege escalation attacks. By the end, you'll understand why every production workload needs its own ServiceAccount, and you'll have the pattern to implement least privilege for any Kubernetes application.
What RBAC Actually Controls
Before creating RBAC resources, understand what you're protecting. Every Kubernetes API request goes through three checkpoints:
| Checkpoint | Question | Denial Result |
|---|---|---|
| Authentication | "Who are you?" | 401 Unauthorized |
| Authorization (RBAC) | "Are you allowed to do this?" | 403 Forbidden |
| Admission Control | "Is this request valid?" | Request rejected |
RBAC handles the second checkpoint. When your Task API pod tries to read a ConfigMap, RBAC answers: "Is this ServiceAccount allowed to get ConfigMaps in this namespace?"
The Four RBAC Building Blocks
RBAC uses four types of resources that work together:
┌─────────────────────────────────────────────────────────────┐
│ RBAC Architecture │
├─────────────────────────────────────────────────────────────┤
│ │
│ SUBJECT BINDING ROLE │
│ (who) (connects) (what) │
│ │
│ ┌───────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ ServiceAccount├────►│ RoleBinding ├────►│ Role │ │
│ │ (task-api-sa) │ │ │ │ (configmap │ │
│ └───────────────┘ └──────────────┘ │ read only) │ │
│ └─────────────┘ │
│ │
│ Namespace-scoped: Role + RoleBinding │
│ Cluster-scoped: ClusterRole + ClusterRoleBinding │
└─────────────────────────────────────────────────────────────┘
| Resource | Scope | Purpose |
|---|---|---|
| ServiceAccount | Namespace | Identity for pods (like a user account for applications) |
| Role | Namespace | Defines what actions are allowed on which resources |
| RoleBinding | Namespace | Connects a ServiceAccount to a Role |
| ClusterRole | Cluster | Like Role, but applies across all namespaces |
| ClusterRoleBinding | Cluster | Connects a ServiceAccount to a ClusterRole |
Step 1: Create a Dedicated ServiceAccount
First, create a namespace for your Task API if it doesn't exist:
kubectl create namespace task-api
Output:
namespace/task-api created
Now create a dedicated ServiceAccount. The critical setting is automountServiceAccountToken: false, which prevents Kubernetes from automatically mounting the token into your pods. Create task-api-sa.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: task-api-sa
namespace: task-api
automountServiceAccountToken: false
Apply the ServiceAccount:
kubectl apply -f task-api-sa.yaml
Output:
serviceaccount/task-api-sa created
Why Disable Auto-Mounting?
By default, Kubernetes mounts a token into every pod at /var/run/secrets/kubernetes.io/serviceaccount/. This token grants API access. If your application doesn't need to call the Kubernetes API, mounting this token only creates attack surface.
Verify the setting:
kubectl get serviceaccount task-api-sa -n task-api -o yaml | grep automount
Output:
automountServiceAccountToken: false
Step 2: Define the Role (Minimum Required Permissions)
Your Task API needs to read its ConfigMap for configuration. It doesn't need to create, update, or delete ConfigMaps. It doesn't need access to Secrets. Define exactly that. Create task-api-role.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: task-api-role
namespace: task-api
rules:
- apiGroups: [""] # Core API group (ConfigMaps, Secrets, Pods, etc.)
resources: ["configmaps"] # Only ConfigMaps
verbs: ["get", "list"] # Read-only operations
Apply the Role:
kubectl apply -f task-api-role.yaml
Output:
role.rbac.authorization.k8s.io/task-api-role created
Understanding the Rules
Each rule has three components:
| Field | Example | Meaning |
|---|---|---|
apiGroups | [""] | Core API group (empty string means v1 resources like ConfigMaps, Pods) |
resources | ["configmaps"] | Which resource types this rule applies to |
verbs | ["get", "list"] | Allowed operations |
Available verbs:
| Verb | kubectl equivalent | Description |
|---|---|---|
get | kubectl get [resource] [name] | Read a single resource |
list | kubectl get [resources] | List all resources of this type |
watch | kubectl get [resources] --watch | Stream updates |
create | kubectl create | Create new resources |
update | kubectl apply (existing) | Modify existing resources |
patch | kubectl patch | Partial modification |
delete | kubectl delete | Remove resources |
Using "*" for apiGroups, resources, or verbs grants far more access than needed. Explicit lists are always safer:
# DANGEROUS - avoid this
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
# SAFE - explicit and minimal
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
Step 3: Create the RoleBinding
The RoleBinding connects your ServiceAccount to the Role. Create task-api-binding.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: task-api-binding
namespace: task-api
subjects:
- kind: ServiceAccount
name: task-api-sa
namespace: task-api
roleRef:
kind: Role
name: task-api-role
apiGroup: rbac.authorization.k8s.io
Apply the RoleBinding:
kubectl apply -f task-api-binding.yaml
Output:
rolebinding.rbac.authorization.k8s.io/task-api-binding created
Step 4: Test RBAC with kubectl auth can-i
Before deploying your application, verify that permissions work as expected. The kubectl auth can-i command tests whether an action is allowed:
Test Allowed Operations
kubectl auth can-i get configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-api
Output:
yes
kubectl auth can-i list configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-api
Output:
yes
Test Denied Operations
kubectl auth can-i create configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-api
Output:
no
kubectl auth can-i get secrets \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-api
Output:
no
kubectl auth can-i get configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n default
Output:
no
The ServiceAccount can read ConfigMaps in its own namespace, but cannot create them, cannot access Secrets, and cannot access resources in other namespaces.
Role vs ClusterRole: The Decision Matrix
When should you use Role (namespace-scoped) versus ClusterRole (cluster-scoped)?
| Scenario | Use | Why |
|---|---|---|
| App reads its own ConfigMap | Role + RoleBinding | Namespace isolation is sufficient |
| App reads ConfigMaps in multiple namespaces | ClusterRole + RoleBinding per namespace | Reuse role definition without cluster-wide access |
| CI/CD creates Deployments across namespaces | ClusterRole + RoleBinding per namespace | Avoid ClusterRoleBinding's full cluster scope |
| Monitoring reads all Pods cluster-wide | ClusterRole + ClusterRoleBinding | Legitimate need for cluster-wide read access |
| Log aggregator collects from all namespaces | ClusterRole + ClusterRoleBinding | Must access every namespace |
Key Principle: Prefer RoleBinding Over ClusterRoleBinding
Even when using a ClusterRole, you can bind it with a RoleBinding to limit scope to one namespace:
# ClusterRole can be reused across namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: configmap-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
# RoleBinding limits to specific namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-configmap-reader
namespace: development
subjects:
- kind: ServiceAccount
name: app-sa
namespace: development
roleRef:
kind: ClusterRole
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
This pattern gives you reusable role definitions without cluster-wide access.
Deploying with the ServiceAccount
To use your ServiceAccount in a Deployment, specify it in the pod spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: task-api
namespace: task-api
spec:
replicas: 2
selector:
matchLabels:
app: task-api
template:
metadata:
labels:
app: task-api
spec:
serviceAccountName: task-api-sa
automountServiceAccountToken: false # Explicit override
containers:
- name: task-api
image: ghcr.io/your-org/task-api:v1.0.0
ports:
- containerPort: 8000
When the pod needs to access the Kubernetes API (for example, to read its ConfigMap), you must explicitly mount the token:
spec:
serviceAccountName: task-api-sa
automountServiceAccountToken: true # Enable when API access needed
Debugging RBAC Issues
When you see "403 Forbidden" errors, use these commands to diagnose:
Check What Permissions a ServiceAccount Has
kubectl auth can-i --list \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-api
Output:
Resources Non-Resource URLs Resource Names Verbs
configmaps [] [] [get list]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
Describe RoleBinding to Verify Configuration
kubectl describe rolebinding task-api-binding -n task-api
Output:
Name: task-api-binding
Namespace: task-api
Role:
Kind: Role
Name: task-api-role
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount task-api-sa task-api
Common RBAC Mistakes
| Symptom | Likely Cause | Fix |
|---|---|---|
| All requests denied | RoleBinding missing or wrong namespace | Check binding exists in correct namespace |
| Can access in one namespace but not another | Using Role instead of ClusterRole | Create RoleBinding in each needed namespace |
| Token not found in pod | automountServiceAccountToken: false | Set to true if API access needed |
| Wrong ServiceAccount used | serviceAccountName not specified | Add serviceAccountName to pod spec |
The Complete RBAC Pattern
Here's the complete set of resources in a single file for easy deployment. Create task-api-rbac-complete.yaml:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: task-api-sa
namespace: task-api
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: task-api-role
namespace: task-api
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: task-api-binding
namespace: task-api
subjects:
- kind: ServiceAccount
name: task-api-sa
namespace: task-api
roleRef:
kind: Role
name: task-api-role
apiGroup: rbac.authorization.k8s.io
Apply everything:
kubectl apply -f task-api-rbac-complete.yaml
Output:
serviceaccount/task-api-sa created
role.rbac.authorization.k8s.io/task-api-role created
rolebinding.rbac.authorization.k8s.io/task-api-binding created
Reflect on Your Skill
Test whether your cloud-security skill generates least-privilege RBAC:
-
Does your skill set
automountServiceAccountToken: falseby default? If not, pods get tokens they might not need. -
Does your skill avoid wildcards in Role rules? Check for
"*"in apiGroups, resources, or verbs. -
Does your skill include the
kubectl auth can-iverification step? Testing before deploying prevents production issues.
If you found gaps, update your skill with the patterns from this lesson. Your skill should now generate the three-resource pattern (ServiceAccount + Role + RoleBinding) with least privilege defaults.
Try With AI
Use your cloud-security skill to practice RBAC design for different scenarios.
Prompt 1:
Using my cloud-security skill, generate RBAC for a metrics-collector
ServiceAccount that needs to read Pod metrics across all namespaces.
Should I use Role or ClusterRole?
What you're learning: This scenario requires cluster-wide read access, which means ClusterRole + ClusterRoleBinding. Notice how the decision matrix guides you from requirements to the correct resource types. The skill should explain why Role won't work here.
Prompt 2:
My pod is getting "forbidden: User ... cannot get resource pods"
errors. Here's my Role definition. What's wrong?
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: monitoring
rules:
- apiGroups: ["apps"]
resources: ["pods"]
verbs: ["get"]
What you're learning: Debugging RBAC errors by analyzing rule definitions. Pods are in the core API group (empty string ""), not "apps". The skill should identify this mismatch and provide the corrected YAML.
Prompt 3:
Audit this RBAC configuration for security issues:
rules:
- apiGroups: ["*"]
resources: ["secrets"]
verbs: ["*"]
What you're learning: Security review of RBAC rules. This rule grants full access to Secrets across all API groups, which violates least privilege. The skill should identify the wildcards as dangerous and suggest specific, minimal permissions instead.
Always test RBAC changes with kubectl auth can-i before deploying. A misconfigured RoleBinding can grant more access than intended or block legitimate operations.