Skip to content

RBAC: Deep Dive

This document explains how Kubernetes RBAC authorization works, why the Role and ClusterRole distinction exists, and when to use aggregated roles, token projection, and least-privilege patterns. It connects the demo manifests to the authorization decisions the API server makes on every request.


Every request to the Kubernetes API server carries an identity. The API server checks: “Does this identity have permission to perform this action on this resource in this namespace?”

RBAC answers this question through four objects:

ObjectScopePurpose
RoleNamespaceDefines permitted operations
ClusterRoleClusterDefines permitted operations (cluster-wide)
RoleBindingNamespaceGrants a Role to subjects
ClusterRoleBindingClusterGrants a ClusterRole to subjects

The relationship is always: Subject is bound to a Role via a Binding.

Subject ──> Binding ──> Role
(who) (link) (what they can do)

Three types of subjects can appear in bindings:

Pod identities. Every pod runs as a ServiceAccount. The demo creates two:

apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-reader
namespace: rbac-demo
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-admin
namespace: rbac-demo

ServiceAccounts are namespaced. Their full identity in RBAC is system:serviceaccount:<namespace>:<name>. For example: system:serviceaccount:rbac-demo:pod-reader.

Human identities. Kubernetes does not manage users directly. Users come from external identity providers (OIDC, X.509 certificates, webhook token authentication). The API server receives a username from the authentication layer and uses it for authorization.

Collections of users. Groups are assigned by the authentication layer. Common built-in groups:

GroupMembers
system:authenticatedAll authenticated users
system:unauthenticatedAll unauthenticated requests
system:serviceaccountsAll ServiceAccounts in all namespaces
system:serviceaccounts:<ns>All ServiceAccounts in namespace <ns>
system:mastersSuperusers (bound to cluster-admin)

A Role defines a list of rules. Each rule specifies:

  • apiGroups: Which API group the resource belongs to.
  • resources: Which resource types are covered.
  • verbs: Which operations are allowed.

The demo defines two Roles:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: rbac-demo
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-admin
namespace: rbac-demo
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]

The apiGroups field identifies which API group contains the resource.

API GroupResources
"" (core)pods, services, configmaps, secrets, namespaces, nodes, persistentvolumeclaims
appsdeployments, statefulsets, daemonsets, replicasets
batchjobs, cronjobs
rbac.authorization.k8s.ioroles, clusterroles, rolebindings, clusterrolebindings
apiextensions.k8s.iocustomresourcedefinitions
networking.k8s.ioingresses, networkpolicies
autoscalinghorizontalpodautoscalers

Some resources have subresources. The demo’s pod-admin Role includes pods/log:

resources: ["pods", "pods/log"]

Subresources are accessed at a different URL path. pods/log is the logs endpoint. Other common subresources:

SubresourcePurpose
pods/logRead container logs
pods/execExecute commands in containers
pods/portforwardPort forwarding
pods/statusRead/update pod status
deployments/scaleScale a deployment
*/statusRead/update status subresource

Granting get on pods does not grant get on pods/log. You must list each subresource explicitly.

The full list of RBAC verbs:

VerbHTTP MethodDescription
getGETRead a single resource by name
listGETList all resources in namespace
watchGET (streaming)Watch for changes
createPOSTCreate a new resource
updatePUTReplace an existing resource
patchPATCHPartially modify a resource
deleteDELETEDelete a single resource
deletecollectionDELETEDelete all resources matching a selector

The wildcard * matches all verbs. Avoid this in production. It grants more access than intended, including deletecollection.


The demo binds each ServiceAccount to its Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader-binding
namespace: rbac-demo
subjects:
- kind: ServiceAccount
name: pod-reader
namespace: rbac-demo
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-admin-binding
namespace: rbac-demo
subjects:
- kind: ServiceAccount
name: pod-admin
namespace: rbac-demo
roleRef:
kind: Role
name: pod-admin
apiGroup: rbac.authorization.k8s.io

Key details:

  • roleRef is immutable. You cannot change which Role a binding references after creation. Delete and recreate the binding instead.
  • subjects is mutable. You can add or remove subjects from an existing binding.
  • One binding, multiple subjects. A single RoleBinding can grant a Role to many ServiceAccounts, users, or groups.

A Role exists in a namespace. A RoleBinding in that namespace grants the Role to subjects within that namespace only.

The demo’s pod-reader can list pods in rbac-demo but not in default:

Terminal window
# Works
kubectl auth can-i list pods \
--as=system:serviceaccount:rbac-demo:pod-reader -n rbac-demo
# Denied
kubectl auth can-i list pods \
--as=system:serviceaccount:rbac-demo:pod-reader -n default

Cluster-Scoped (ClusterRole + ClusterRoleBinding)

Section titled “Cluster-Scoped (ClusterRole + ClusterRoleBinding)”

A ClusterRole is not namespaced. A ClusterRoleBinding grants it across all namespaces.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-pod-reader-binding
subjects:
- kind: ServiceAccount
name: pod-reader
namespace: rbac-demo
roleRef:
kind: ClusterRole
name: cluster-pod-reader
apiGroup: rbac.authorization.k8s.io

After this binding, pod-reader can list pods in every namespace.

A powerful combination. A ClusterRole defines permissions once. A RoleBinding in a specific namespace grants those permissions only within that namespace.

This avoids duplicating identical Role definitions across namespaces. Define the permissions as a ClusterRole. Grant them per-namespace with RoleBindings.

Some resources are not namespaced: nodes, namespaces, persistentvolumes, clusterroles, clusterrolebindings. Access to these requires a ClusterRole and ClusterRoleBinding. A namespace-scoped Role cannot grant access to cluster-scoped resources.


Aggregated ClusterRoles combine rules from multiple ClusterRoles using label selectors:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-aggregate
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.example.com/aggregate-to-monitoring: "true"
rules: [] # Rules are auto-populated by the controller

Any ClusterRole with the label rbac.example.com/aggregate-to-monitoring: "true" automatically contributes its rules to monitoring-aggregate.

This is how the built-in admin, edit, and view ClusterRoles work. When you install a CRD, you can add rules to these roles by creating a ClusterRole with the right label:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: website-viewer
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
- apiGroups: ["demo.example.com"]
resources: ["websites"]
verbs: ["get", "list", "watch"]

Now anyone with the view ClusterRole can also read Website custom resources.


Every namespace has a default ServiceAccount. Pods that do not specify a ServiceAccount run as default. In older Kubernetes versions (before 1.24), this ServiceAccount had a long-lived token secret auto-created.

Modern Kubernetes uses bound service account tokens. These are short-lived, audience-scoped JWTs projected into the pod via a projected volume:

volumes:
- name: kube-api-access
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: https://kubernetes.default.svc

The kubelet automatically refreshes the token before it expires. The token is valid only for the specified audience and expiration time.

Old-style tokens never expired. If a token leaked, it was valid forever. Projected tokens expire. They are scoped to a specific audience. Rotating them is automatic.

Best practice: do not rely on the default ServiceAccount for application permissions. Create dedicated ServiceAccounts and bind them to the minimum required roles.


On every API request:

  1. Authentication. The API server identifies the caller (ServiceAccount token, client certificate, OIDC token, etc.).
  2. Authorization. The API server checks all RoleBindings and ClusterRoleBindings that reference the caller’s identity.
  3. Match. If any binding grants the requested verb on the requested resource in the requested namespace, the request is allowed.
  4. Deny by default. If no binding matches, the request is denied.

RBAC is additive. You can only grant permissions, never deny them. There is no “deny” rule. If you need to restrict a broad grant, you must restructure your bindings to be more specific.

Some API paths are not resource-based: /healthz, /apis, /version, /openapi/v2. Access to these is controlled via ClusterRoles with nonResourceURLs:

rules:
- nonResourceURLs: ["/healthz", "/healthz/*"]
verbs: ["get"]

Do not share ServiceAccounts across different applications. If app A and app B use the same ServiceAccount, they have identical permissions. A compromise of either app grants access to both sets of resources.

Start with get, list, watch. Add create, update, delete only when needed.

# Too broad
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]

This is cluster-admin. It grants everything. Never use wildcards in production roles.

Restrict access to specific named resources:

rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"]
verbs: ["get", "update"]

This grants read and update access to only the ConfigMap named app-config. All other ConfigMaps are inaccessible.

Create separate roles for different access levels:

# Reader role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# Writer role (additive to reader)
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "update", "delete"]

Bind the reader role to monitoring tools. Bind both roles to operators that need write access.


Pods that can get secrets can read every secret in the namespace, including database passwords, API keys, and TLS certificates. Be very selective about who gets secrets access.

A Role with get on pods cannot read pod logs. You need pods/log. A Role with update on deployments cannot scale them. You need deployments/scale.

Using a ClusterRoleBinding when you meant RoleBinding grants cluster-wide access. Always double-check the binding scope.

The system:masters group is bound to cluster-admin. Adding users to this group gives them unrestricted access. Use it only for break-glass scenarios, not for day-to-day operations.

If you do not set automountServiceAccountToken: false on pods that do not need API access, every pod can make authenticated requests to the API server. By default, the default ServiceAccount has no extra permissions, but it can still access the discovery API.

spec:
automountServiceAccountToken: false

The demo uses kubectl auth can-i to verify permissions:

Terminal window
# Can pod-reader list pods in rbac-demo?
kubectl auth can-i list pods \
--as=system:serviceaccount:rbac-demo:pod-reader -n rbac-demo
# yes
# Can pod-reader delete pods?
kubectl auth can-i delete pods \
--as=system:serviceaccount:rbac-demo:pod-reader -n rbac-demo
# no
# List all permissions for pod-admin
kubectl auth can-i --list \
--as=system:serviceaccount:rbac-demo:pod-admin -n rbac-demo

The --as flag impersonates a subject. This requires the caller to have impersonation permissions (typically granted to cluster admins).


Custom resources need explicit RBAC rules. The apiGroups field must match the CRD’s group:

rules:
- apiGroups: ["demo.example.com"]
resources: ["websites", "websites/status"]
verbs: ["get", "list", "watch", "update", "patch"]

Without these rules, no ServiceAccount can read or modify Website custom resources, even if they have broad permissions on core resources.


The demo creates a minimal but complete RBAC setup:

  1. ServiceAccounts: pod-reader and pod-admin provide identities.
  2. Roles: Define different permission levels. pod-reader can only read pods. pod-admin can read, create, and delete pods, plus read services and deployments.
  3. RoleBindings: Connect each ServiceAccount to its Role.
  4. Test pods: A sample Deployment provides resources to test against.

The permission boundary is clear. pod-reader cannot delete pods. pod-admin cannot create deployments. Neither can access resources in other namespaces.