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.
The RBAC Authorization Model
Section titled “The RBAC Authorization Model”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:
| Object | Scope | Purpose |
|---|---|---|
| Role | Namespace | Defines permitted operations |
| ClusterRole | Cluster | Defines permitted operations (cluster-wide) |
| RoleBinding | Namespace | Grants a Role to subjects |
| ClusterRoleBinding | Cluster | Grants 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)Subjects: Who Is Requesting
Section titled “Subjects: Who Is Requesting”Three types of subjects can appear in bindings:
ServiceAccounts
Section titled “ServiceAccounts”Pod identities. Every pod runs as a ServiceAccount. The demo creates two:
apiVersion: v1kind: ServiceAccountmetadata: name: pod-reader namespace: rbac-demo---apiVersion: v1kind: ServiceAccountmetadata: name: pod-admin namespace: rbac-demoServiceAccounts 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.
Groups
Section titled “Groups”Collections of users. Groups are assigned by the authentication layer. Common built-in groups:
| Group | Members |
|---|---|
system:authenticated | All authenticated users |
system:unauthenticated | All unauthenticated requests |
system:serviceaccounts | All ServiceAccounts in all namespaces |
system:serviceaccounts:<ns> | All ServiceAccounts in namespace <ns> |
system:masters | Superusers (bound to cluster-admin) |
Roles: What Is Permitted
Section titled “Roles: What Is Permitted”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/v1kind: Rolemetadata: name: pod-reader namespace: rbac-demorules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: pod-admin namespace: rbac-demorules: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: [""] resources: ["services"] verbs: ["get", "list"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list"]API Groups
Section titled “API Groups”The apiGroups field identifies which API group contains the resource.
| API Group | Resources |
|---|---|
"" (core) | pods, services, configmaps, secrets, namespaces, nodes, persistentvolumeclaims |
apps | deployments, statefulsets, daemonsets, replicasets |
batch | jobs, cronjobs |
rbac.authorization.k8s.io | roles, clusterroles, rolebindings, clusterrolebindings |
apiextensions.k8s.io | customresourcedefinitions |
networking.k8s.io | ingresses, networkpolicies |
autoscaling | horizontalpodautoscalers |
Subresources
Section titled “Subresources”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:
| Subresource | Purpose |
|---|---|
pods/log | Read container logs |
pods/exec | Execute commands in containers |
pods/portforward | Port forwarding |
pods/status | Read/update pod status |
deployments/scale | Scale a deployment |
*/status | Read/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:
| Verb | HTTP Method | Description |
|---|---|---|
get | GET | Read a single resource by name |
list | GET | List all resources in namespace |
watch | GET (streaming) | Watch for changes |
create | POST | Create a new resource |
update | PUT | Replace an existing resource |
patch | PATCH | Partially modify a resource |
delete | DELETE | Delete a single resource |
deletecollection | DELETE | Delete all resources matching a selector |
The wildcard * matches all verbs. Avoid this in production. It grants more access than
intended, including deletecollection.
Bindings: Connecting Subjects to Roles
Section titled “Bindings: Connecting Subjects to Roles”The demo binds each ServiceAccount to its Role:
apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: pod-reader-binding namespace: rbac-demosubjects: - kind: ServiceAccount name: pod-reader namespace: rbac-demoroleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: pod-admin-binding namespace: rbac-demosubjects: - kind: ServiceAccount name: pod-admin namespace: rbac-demoroleRef: kind: Role name: pod-admin apiGroup: rbac.authorization.k8s.ioKey details:
roleRefis immutable. You cannot change which Role a binding references after creation. Delete and recreate the binding instead.subjectsis 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.
Role vs ClusterRole
Section titled “Role vs ClusterRole”Namespace-Scoped (Role + RoleBinding)
Section titled “Namespace-Scoped (Role + RoleBinding)”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:
# Workskubectl auth can-i list pods \ --as=system:serviceaccount:rbac-demo:pod-reader -n rbac-demo
# Deniedkubectl auth can-i list pods \ --as=system:serviceaccount:rbac-demo:pod-reader -n defaultCluster-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/v1kind: ClusterRolemetadata: name: cluster-pod-readerrules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: cluster-pod-reader-bindingsubjects: - kind: ServiceAccount name: pod-reader namespace: rbac-demoroleRef: kind: ClusterRole name: cluster-pod-reader apiGroup: rbac.authorization.k8s.ioAfter this binding, pod-reader can list pods in every namespace.
ClusterRole + RoleBinding
Section titled “ClusterRole + RoleBinding”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.
Cluster-Scoped Resources
Section titled “Cluster-Scoped Resources”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
Section titled “Aggregated ClusterRoles”Aggregated ClusterRoles combine rules from multiple ClusterRoles using label selectors:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: monitoring-aggregateaggregationRule: clusterRoleSelectors: - matchLabels: rbac.example.com/aggregate-to-monitoring: "true"rules: [] # Rules are auto-populated by the controllerAny 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/v1kind: ClusterRolemetadata: 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.
The Default ServiceAccount
Section titled “The Default ServiceAccount”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.
Token Projection (Kubernetes 1.22+)
Section titled “Token Projection (Kubernetes 1.22+)”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.svcThe kubelet automatically refreshes the token before it expires. The token is valid only for the specified audience and expiration time.
Why This Matters for Security
Section titled “Why This Matters for Security”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.
How the API Server Evaluates RBAC
Section titled “How the API Server Evaluates RBAC”On every API request:
- Authentication. The API server identifies the caller (ServiceAccount token, client certificate, OIDC token, etc.).
- Authorization. The API server checks all RoleBindings and ClusterRoleBindings that reference the caller’s identity.
- Match. If any binding grants the requested verb on the requested resource in the requested namespace, the request is allowed.
- 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.
Non-Resource URLs
Section titled “Non-Resource URLs”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"]Least-Privilege Patterns
Section titled “Least-Privilege Patterns”One ServiceAccount Per Workload
Section titled “One ServiceAccount Per Workload”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.
Read-Only First, Add Write Later
Section titled “Read-Only First, Add Write Later”Start with get, list, watch. Add create, update, delete only when needed.
Avoid Wildcards
Section titled “Avoid Wildcards”# Too broadrules: - apiGroups: ["*"] resources: ["*"] verbs: ["*"]This is cluster-admin. It grants everything. Never use wildcards in production roles.
Use resourceNames for Granular Access
Section titled “Use resourceNames for Granular Access”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.
Separate Read and Write Roles
Section titled “Separate Read and Write Roles”Create separate roles for different access levels:
# Reader rolerules: - 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.
Common Mistakes
Section titled “Common Mistakes”Granting Secrets Access
Section titled “Granting Secrets 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.
Forgetting Subresources
Section titled “Forgetting Subresources”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.
Binding a ClusterRole to the Wrong Scope
Section titled “Binding a ClusterRole to the Wrong Scope”Using a ClusterRoleBinding when you meant RoleBinding grants cluster-wide access. Always double-check the binding scope.
Over-Relying on system:masters
Section titled “Over-Relying on system:masters”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.
Ignoring the Default ServiceAccount
Section titled “Ignoring the Default ServiceAccount”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: falseTesting Permissions
Section titled “Testing Permissions”The demo uses kubectl auth can-i to verify permissions:
# 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-adminkubectl auth can-i --list \ --as=system:serviceaccount:rbac-demo:pod-admin -n rbac-demoThe --as flag impersonates a subject. This requires the caller to have impersonation
permissions (typically granted to cluster admins).
RBAC for CRDs
Section titled “RBAC for CRDs”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.
Connection to the Demo
Section titled “Connection to the Demo”The demo creates a minimal but complete RBAC setup:
- ServiceAccounts:
pod-readerandpod-adminprovide identities. - Roles: Define different permission levels.
pod-readercan only read pods.pod-admincan read, create, and delete pods, plus read services and deployments. - RoleBindings: Connect each ServiceAccount to its Role.
- 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.
Further Reading
Section titled “Further Reading”- Kubernetes RBAC documentation
- Using RBAC Authorization
- Service Account Token Volume Projection
- Pod Security Admission