RBAC — Roles, Bindings & ServiceAccounts
By default, every pod in a Kubernetes cluster runs with a ServiceAccount that can authenticate to the API server. Without RBAC, that's a foothold — a compromised pod can list all Secrets, read all ConfigMaps, or even create new pods. Role-Based Access Control (RBAC) is how you define exactly which API resources each identity can access, and which operations they can perform on them.
Subjects
RBAC grants permissions to subjects — the "who" in an access control rule. Kubernetes has three subject types:
| Type | What it is | Scope |
|---|---|---|
| User | Human identity — authenticated via certificates, OIDC, or webhook. Kubernetes has no built-in user store. | Cluster-wide name |
| Group | Set of users. Defined by the authentication provider (OIDC groups, certificate O= field). | Cluster-wide name |
| ServiceAccount | Machine identity for pods. Kubernetes-managed. Namespaced. | Namespace-scoped |
Role vs ClusterRole
A Role grants permissions within a single namespace. A ClusterRole grants permissions cluster-wide — or is used as a template that RoleBindings reference to grant namespace-scoped permissions.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""] # "" = core API group (pods, services, secrets…)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"] # nodes are cluster-scoped — only ClusterRole works
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes", "pods"]
verbs: ["get", "list"]
Common verbs: get, list, watch, create, update, patch, delete, deletecollection. For sub-resources like pods/exec or pods/portforward, list them explicitly in resources.
RoleBinding vs ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-can-read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: ci-runner
namespace: ci # ServiceAccount can be in a different namespace
- kind: User
name: alice@example.com # OIDC-authenticated user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role # or ClusterRole
name: pod-reader
apiGroup: rbac.authorization.k8s.io
A ClusterRoleBinding grants permissions cluster-wide — for all namespaces and cluster-scoped resources. Use sparingly. Prefer a RoleBinding + ClusterRole combination when you want a reusable role template applied per-namespace.
ServiceAccounts
A ServiceAccount is a namespaced identity for pods. When a pod starts, Kubernetes automatically mounts the ServiceAccount's token at /var/run/secrets/kubernetes.io/serviceaccount/token (a projected JWT). The pod uses this token to authenticate to the API server.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
annotations:
# AWS: associate IAM role (IRSA) — pod gets AWS credentials automatically
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-app-role
# GCP: Workload Identity
iam.gke.io/gcp-service-account: my-app@my-project.iam.gserviceaccount.com
automountServiceAccountToken: false # don't mount token unless explicitly needed
spec:
serviceAccountName: app-sa # instead of default
automountServiceAccountToken: false # opt out if the app doesn't call the K8s API
Default ServiceAccount Risks
Every namespace has a default ServiceAccount. Without any configuration, pods run as this account. If you accidentally grant broad ClusterRole permissions to the system:authenticated group or the default ServiceAccount, every pod in the cluster inherits those permissions.
Best practices:
- Create a dedicated ServiceAccount per application
- Set
automountServiceAccountToken: falseon ServiceAccounts whose pods don't call the Kubernetes API - Set
automountServiceAccountToken: falseon thedefaultServiceAccount in each namespace to prevent token mounting by accident - Never bind ClusterAdmin to a ServiceAccount that pods run under
Least-Privilege Patterns
# Example: an app that needs to read its own ConfigMap and Secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-config-reader
namespace: production
rules:
# Read specific ConfigMap and Secret by resource name
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"] # scope to specific resource names
verbs: ["get"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-secrets"]
verbs: ["get"]
# Watch Endpoints for its own Service (for leader election)
- apiGroups: [""]
resources: ["endpoints"]
resourceNames: ["my-app-lock"]
verbs: ["get", "update", "patch"]
The list verb on secrets returns all Secret data in the namespace — not just metadata. Be especially careful with the list and watch verbs on Secrets. Grant get with specific resourceNames when an app only needs its own credentials.
Aggregated ClusterRoles
Kubernetes extends built-in ClusterRoles (view, edit, admin) through aggregation. Any ClusterRole with a matching label is automatically merged into the aggregated role — useful for CRDs whose resources should be visible to users with the standard view role.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: certificates-viewer
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true" # merged into built-in "view" role
rules:
- apiGroups: ["cert-manager.io"]
resources: ["certificates", "certificaterequests"]
verbs: ["get", "list", "watch"]
Debugging Forbidden Errors
When you get a 403 Forbidden, use these tools to diagnose:
# Check if an identity can perform an action
kubectl auth can-i get pods -n production --as=system:serviceaccount:production:app-sa
# yes / no
# List all permissions a ServiceAccount has (impersonate and list rules)
kubectl auth can-i --list -n production --as=system:serviceaccount:production:app-sa
# Check which RoleBindings exist for a subject
kubectl get rolebindings,clusterrolebindings -A \
-o json | jq '.items[] | select(
.subjects[]? |
.kind == "ServiceAccount" and .name == "app-sa" and .namespace == "production"
) | .metadata.name'
# View all rules in a Role
kubectl describe role app-config-reader -n production
# View all rules in a ClusterRole
kubectl describe clusterrole view
# Check audit logs for denied requests (if audit logging is enabled)
kubectl logs -n kube-system kube-apiserver-* | grep '"response_code":403'
kubectl Commands
# List Roles and ClusterRoles
kubectl get role -n production
kubectl get clusterrole | grep -v system:
# List RoleBindings and ClusterRoleBindings
kubectl get rolebinding -n production
kubectl get clusterrolebinding
# Describe a RoleBinding (shows subject and role ref)
kubectl describe rolebinding ci-can-read-pods -n production
# Create a ServiceAccount
kubectl create serviceaccount app-sa -n production
# Create a Role (dry-run, output YAML)
kubectl create role pod-reader \
--verb=get,list,watch \
--resource=pods,pods/log \
-n production \
--dry-run=client -o yaml
# Bind a Role to a ServiceAccount
kubectl create rolebinding app-binding \
--role=pod-reader \
--serviceaccount=production:app-sa \
-n production
# Remove a binding
kubectl delete rolebinding app-binding -n production