Security

RBAC — Roles, Bindings & ServiceAccounts

● Intermediate ⏱ 15 min read

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:

TypeWhat it isScope
UserHuman identity — authenticated via certificates, OIDC, or webhook. Kubernetes has no built-in user store.Cluster-wide name
GroupSet of users. Defined by the authentication provider (OIDC groups, certificate O= field).Cluster-wide name
ServiceAccountMachine 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.

role.yaml — namespace-scoped
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"]
clusterrole.yaml — cluster-scoped
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.

RBAC resource hierarchy and binding combinations
Namespace-scoped
namespace: production
Role
pod-reader
RoleBinding
ci-can-read-pods
ServiceAccount
ci-runner
Cluster-scoped
cluster-wide
ClusterRole
view
ClusterRoleBinding
devs-can-view-all
Group
dev-team
Note: a RoleBinding can reference a ClusterRole — grants that ClusterRole's permissions but scoped to the RoleBinding's namespace only
Role and ClusterRole define permissions. Bindings connect subjects to roles. A ClusterRole bound by a RoleBinding grants namespace-scoped access only.

RoleBinding vs ClusterRoleBinding

rolebinding.yaml
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.

serviceaccount.yaml
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
pod using a specific ServiceAccount
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:

Least-Privilege Patterns

least-privilege-role.yaml
# 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"]
💡
list secrets = read all secrets in that namespace

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