RBAC Deep Dive
Implement Kubernetes RBAC for Task API with ServiceAccounts, Roles, and RoleBindings following the principle of least privilege
- Status
- stable
- Trust
- unverified
- Approved
- ag-archive/0.1.0 ·
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-apiOutput:
namespace/task-api createdNow 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: falseApply the ServiceAccount:
kubectl apply -f task-api-sa.yamlOutput:
serviceaccount/task-api-sa createdWhy 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 automountOutput:
automountServiceAccountToken: falseStep 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 operationsApply the Role:
kubectl apply -f task-api-role.yamlOutput:
role.rbac.authorization.k8s.io/task-api-role createdUnderstanding 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 |
Warning
Never Use Wildcards
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.ioApply the RoleBinding:
kubectl apply -f task-api-binding.yamlOutput:
rolebinding.rbac.authorization.k8s.io/task-api-binding createdStep 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-apiOutput:
yeskubectl auth can-i list configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-apiOutput:
yesTest Denied Operations
kubectl auth can-i create configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-apiOutput:
nokubectl auth can-i get secrets \
--as=system:serviceaccount:task-api:task-api-sa \
-n task-apiOutput:
nokubectl auth can-i get configmaps \
--as=system:serviceaccount:task-api:task-api-sa \
-n defaultOutput:
noThe 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.ioThis 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: 8000When 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 neededDebugging 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-apiOutput:
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-apiOutput:
Name: task-api-binding
Namespace: task-api
Role:
Kind: Role
Name: task-api-role
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount task-api-sa task-apiCommon 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.ioApply everything:
kubectl apply -f task-api-rbac-complete.yamlOutput:
serviceaccount/task-api-sa created
role.rbac.authorization.k8s.io/task-api-role created
rolebinding.rbac.authorization.k8s.io/task-api-binding createdReflect 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.
Warning
Security Reminder
Always test RBAC changes with kubectl auth can-i before deploying. A misconfigured RoleBinding can grant more access than intended or block legitimate operations.
Core Concept
RBAC enforces least privilege in Kubernetes: bind a dedicated ServiceAccount to narrowly scoped Roles/ClusterRoles so pods can do only what they must—and nothing more.
Key Mental Models
- Four-piece model: ServiceAccount (who) → Role/ClusterRole (what) → Binding (connect) → Scope (namespace vs cluster).
- Least privilege: Deny by default; never use wildcards; disable token automount unless needed.
- Verification loop: Use
kubectl auth can-ito confirm allowed/denied actions before rollout.
Critical Patterns
- Create per-app ServiceAccounts with
automountServiceAccountToken: false; mount tokens explicitly when needed. - Define Roles with specific resources/verbs; use ClusterRoles only when cross-namespace access is required.
- Bind SA to Role with RoleBinding (or ClusterRoleBinding for cluster scope).
- Test access via
kubectl auth can-i --as=system:serviceaccount:...and enforce namespace scoping.
Common Mistakes
- Using default service account or cluster-admin-level bindings for apps.
- Wildcarding resources/verbs or granting cluster-wide access unnecessarily.
- Skipping verification, leaving broken or overprivileged permissions in production.
Connections
- Builds on: 4C security model.
- Leads to: NetworkPolicies, PSS, secrets management, and Dapr security controls in later lessons.
Cloud Native Security Model
Learn the 4C security model (Cloud, Cluster, Container, Code) and understand why defense in depth is essential for Kubernetes workloads
NetworkPolicies for Zero-Trust Traffic Control
Implement default-deny NetworkPolicies with explicit allow rules to reduce blast radius and control east-west traffic in your Kubernetes cluster