Kubernetes RBAC
Role-Based Access Control (RBAC) is the standard authorization mechanism in Kubernetes — the authorizer that decides whether a given identity may perform a given verb on a given resource. It is the dominant mode in the
Node,RBACchain described in Kubernetes Authorization Modes, and the most security-critical configurable surface in a cluster. RBAC is built from exactly four object types in therbac.authorization.k8s.io/v1API group —Role,ClusterRole,RoleBinding,ClusterRoleBinding(Kubernetes — Using RBAC). The two*Roleobjects define sets of permissions; the two `Binding objects attach those permission sets to subjects (users, groups, ServiceAccounts). The single most important property to internalize: RBAC is purely additive — there are no deny rules. You grant; you never subtract. A subject’s effective permissions are the union of every rule reachable through every binding that names them.
Mental Model
RBAC is a bipartite graph with a scope dimension. On one side are permission sets (Role, ClusterRole); on the other are subjects (User, Group, ServiceAccount). Bindings (RoleBinding, ClusterRoleBinding) are the edges. The orthogonal axis is scope: namespaced vs cluster-wide.
flowchart LR subgraph subjects["Subjects (not RBAC objects — identities)"] U["User<br/>jane@example.com"] G["Group<br/>developers"] SA["ServiceAccount<br/>payments/ci-deployer"] end subgraph perms["Permission sets"] R["Role<br/>(namespaced rules)"] CR["ClusterRole<br/>(cluster-scoped rules<br/>or reusable template)"] end subgraph bindings["Bindings (the edges)"] RB["RoleBinding<br/>grants WITHIN one namespace"] CRB["ClusterRoleBinding<br/>grants CLUSTER-WIDE"] end U --> RB G --> RB SA --> RB U --> CRB G --> CRB SA --> CRB RB -->|"may reference"| R RB -->|"may also reference"| CR CRB -->|"references only"| CR
The diagram shows the four objects and their legal connections. The insight to extract: a RoleBinding can reference either a Role or a ClusterRole — referencing a ClusterRole from a RoleBinding reuses a cluster-defined permission template but grants it only within the binding’s namespace. A ClusterRoleBinding can reference only a ClusterRole, and always grants cluster-wide. There is deliberately no “ClusterRole referenced by RoleBinding gives cluster-wide access” path — the binding’s kind, not the role’s kind, determines scope.
Mechanical Walk-through
Roles — the permission sets
A Role is namespaced. Its rules array is a list of policy rules, each a tuple:
apiGroups— which API group(s) the rule covers.[""]is the core group (Pods, Services, ConfigMaps, Secrets, …);["apps"]covers Deployments/ReplicaSets/StatefulSets/DaemonSets;["rbac.authorization.k8s.io"]covers RBAC objects themselves.resources— which resource types:["pods"],["deployments"]. Subresources are slash-suffixed:pods/log,pods/exec,deployments/scale.verbs— which actions (below).resourceNames(optional) — restrict the rule to named objects.resourceNames: ["app-config"]on aconfigmapsrule means “this verb on the ConfigMap namedapp-configonly.”resourceNamescannot restrictlist,watch,create, ordeletecollection(those operate on collections, not a name).
A ClusterRole has the same rule shape but is cluster-scoped, giving it two distinct uses: (1) granting access to cluster-scoped resources — Nodes, PersistentVolumes, Namespaces, the RBAC objects themselves — and to non-resource URLs like /healthz, which a namespaced Role cannot express; and (2) acting as a reusable permission template that a RoleBinding in any namespace can reference, so “read access to ConfigMaps” is defined once as a ClusterRole and bound per-namespace many times.
Verbs
The standard verbs (Using RBAC):
| Category | Verbs |
|---|---|
| Read | get, list, watch |
| Write | create, update, patch |
| Delete | delete, deletecollection |
| Special | bind, escalate, impersonate, use |
list and get are distinct — get reads one named object, list enumerates a collection (and a list permission exposes the contents of every object in it, which is why “get on Secrets” and “list Secrets” are very different grants). The special verbs gate specific capabilities: bind and escalate gate privilege-escalation paths (below); impersonate lets a subject act as another user/group/ServiceAccount (the --as flag); use gates use of a constrained resource (historically PodSecurityPolicy, still relevant for some policy objects).
Bindings — attaching permissions to subjects
A RoleBinding lives in a namespace and connects a roleRef (a Role in the same namespace, or a ClusterRole) to a list of subjects. A subject is one of:
kind: User— an identity string from authentication (a certCN, an OIDC username). Kubernetes has no User object; the name is just a string.kind: Group— a group string (a certOfield, an OIDC group claim, or a built-in likesystem:authenticated).kind: ServiceAccount— a real, namespaced API object; namedname+namespace.
A RoleBinding grants its permissions only within its own namespace, regardless of whether roleRef points at a Role or a ClusterRole.
A ClusterRoleBinding connects a ClusterRole (only — never a Role) to subjects, and grants those permissions across every namespace and on all cluster-scoped resources. A ClusterRoleBinding is therefore the most dangerous RBAC object: an over-broad one is a cluster-wide grant.
Additive evaluation — no deny
When a request arrives, the RBAC authorizer collects every binding that names the request’s user or any of its groups (or its ServiceAccount), expands each binding’s referenced role into its rules, and checks whether any rule matches the request’s (verb, apiGroup, resource, [name]) tuple. If one does, RBAC returns allow; if none does, RBAC returns no opinion — never deny. This is the defining property: you cannot write an RBAC rule that removes a permission another rule grants. Effective access is the union of all matching rules. To deny something, you must ensure no binding grants it — there is no subtractive override. (If you need genuine deny semantics, that requires an admission policy or an authorization webhook ordered before RBAC — see Kubernetes Authorization Modes.)
Privilege escalation prevention
If RBAC let any user create arbitrary Roles, a user could simply write themselves a cluster-admin-equivalent Role. Two guardrails prevent this (Using RBAC — Privilege escalation prevention):
- The
escalaterule. When youcreateorupdateaRole/ClusterRole, the apiserver checks that every permission in the new role is one you already hold. You cannot create a role grantingdeleteon Secrets unless you can alreadydeleteSecrets. The exception: a subject that holds theescalateverb onroles/clusterrolesmay write roles with permissions beyond their own. - The
bindrule. When you create aRoleBinding/ClusterRoleBinding, the apiserver checks that you already hold all the permissions in the role being bound — otherwise you could grant others access you cannot grant yourself, then assume that ServiceAccount. The exception: holding thebindverb on the referenced role lets you bind it regardless.
Together these mean RBAC is closed under self-modification: with neither escalate nor bind, you can never produce more privilege than you started with.
Aggregated ClusterRoles
A ClusterRole may carry an aggregationRule instead of (or alongside) explicit rules. The aggregation rule holds label selectors; the RBAC controller continuously sets that ClusterRole’s rules to the union of the rules of every other ClusterRole whose labels match the selector. This is how the built-in admin/edit/view roles automatically pick up permissions for Custom Resources: a CRD operator ships a small ClusterRole labelled rbac.authorization.k8s.io/aggregate-to-edit: "true", and its rules silently merge into the cluster’s edit role without anyone editing edit directly.
Default roles
The apiserver bootstraps a set of default ClusterRoles and bindings so a fresh cluster is neither locked out nor wide open. The user-facing four (Default Roles):
cluster-admin— unrestricted: every verb on every resource, all namespaces, plus non-resource URLs. Bound cluster-wide to thesystem:mastersgroup by thecluster-adminClusterRoleBinding.admin— full read/write within a namespace including RBACRole/RoleBindingmanagement, but not namespace-quota objects; intended to be bound with aRoleBinding.edit— read/write on most namespaced resources but not Roles/RoleBindings (so anedituser cannot escalate themselves). The default “developer” role.view— read-only on most namespaced resources, and notably excludes Secrets (aviewuser cannot read Secret contents).
There are also dozens of system:* ClusterRoles for the control-plane components themselves (system:kube-scheduler, system:node, system:controller:*).
Configuration / API Surface
A complete least-privilege grant: a CI ServiceAccount that may manage Deployments in one namespace.
# 1. A namespaced Role: the permission set.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: payments
name: deployment-manager
rules:
- apiGroups: ["apps"] # Deployments live in the 'apps' group
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# note: NO 'delete' — CI may roll, not destroy
- apiGroups: [""] # core group
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"] # read-only on Pods + their logs, for rollout checks
---
# 2. A RoleBinding: attach the Role to a subject, scoped to this namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: payments
name: ci-deployer-can-deploy
subjects:
- kind: ServiceAccount # the subject is an in-cluster identity
name: ci-deployer
namespace: payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role # references the Role above (could be ClusterRole)
name: deployment-managerLine-by-line. The Role is namespace: payments — every rule applies only there. The first rule deliberately omits delete and deletecollection: CI should roll Deployments forward, not delete them — RBAC, being additive, expresses “no delete” simply by not granting it. The second rule grants read on pods/log so the pipeline can tail rollout logs. The RoleBinding’s roleRef.kind: Role references the namespaced Role; the subjects entry names the ServiceAccount by name+namespace. The grant is confined to payments because the binding is a RoleBinding — to make it cluster-wide you would need a ClusterRoleBinding (and would have to think very hard before doing so).
# Verify the effective grant rather than re-reading YAML by hand:
kubectl auth can-i create deployments \
--as system:serviceaccount:payments:ci-deployer -n payments # → yes
kubectl auth can-i delete deployments \
--as system:serviceaccount:payments:ci-deployer -n payments # → no
# Imperative shortcuts (useful for quick grants; prefer YAML under GitOps):
kubectl create role pod-reader --verb=get,list,watch --resource=pods -n dev
kubectl create rolebinding alice-reads --role=pod-reader --user=alice -n devFailure Modes
Wildcard verbs/resources. verbs: ["*"] + resources: ["*"] + apiGroups: ["*"] is a namespace-wide superuser; in a ClusterRole bound by a ClusterRoleBinding it is cluster-admin by another name. Wildcards also silently absorb future resource types (a CRD installed next month). Enumerate verbs and resources explicitly.
Over-broad ClusterRoleBinding. Binding edit (or worse cluster-admin) to a group like system:authenticated via a ClusterRoleBinding grants it to every authenticated identity, every ServiceAccount included. The most common real-world RBAC incident. Audit: kubectl get clusterrolebindings -o wide.
system:masters bypass. The group system:masters is bound to cluster-admin and the apiserver short-circuits authorization for it entirely — no RBAC rule, not even a hypothetical deny, constrains it. A leaked credential in system:masters is an unrevocable cluster takeover until the CA is rotated. Treat the kubeadm admin kubeconfig accordingly.
get Secrets confused with list Secrets. get needs the object name; list dumps every Secret in scope. A Role meant to “read one config Secret” that grants list on secrets instead exposes all of them. Use resourceNames to pin get/watch to specific Secrets, and never grant list on Secrets unless intended.
Granting escalate / bind / impersonate casually. escalate lets a subject write roles beyond their own permissions; bind lets them bind roles they don’t hold; impersonate lets them become another principal. Any of the three converts a limited account into a path to full privilege. They should appear in almost no custom role.
ServiceAccount left on the default SA. A Pod with no serviceAccountName runs as the namespace default ServiceAccount. If anyone has bound permissions to default (or to the group system:serviceaccounts), every Pod in the namespace silently inherits them. Give workloads dedicated, minimally-scoped ServiceAccounts — see ServiceAccount.
Alternatives and When to Choose Them
- RBAC vs ABAC. RBAC is dynamic, API-driven, auditable, and the default; ABAC is a static restart-required file. Use RBAC.
- RBAC vs admission policy for “deny” semantics. RBAC cannot deny — only fail to grant. When you need a genuine prohibition that holds regardless of what else is granted (“no Pod may be privileged,” “no one may delete this namespace”), that is an admission concern: ValidatingAdmissionPolicy, OPA Gatekeeper, or Kyverno. RBAC controls who may call the API; admission controls what objects are acceptable.
- Per-namespace
RoleBindingto a sharedClusterRolevs manyRoles. Define the permission set once as aClusterRoleand bind it per namespace — DRY, and a fix to the permission set propagates everywhere. Bespoke per-namespaceRoles only when the permissions genuinely differ. - Aggregated ClusterRoles vs editing built-ins. Never edit
admin/edit/viewdirectly — your edits are not reconciled and can be reverted on upgrade. Add a labelled ClusterRole and let aggregation merge it.
Production Notes
- Least privilege, audited continuously. Start every workload from zero permissions and add only what
kubectl auth can-i --listshows it actually needs. Tools likerbac-lookup,rbac-tool, andkubectl who-canreverse the graph (“who can delete Secrets cluster-wide?”) and belong in CI. viewexcludes Secrets by design. A common mistake is assumingviewis harmless read-only — it is, and it deliberately cannot read Secret contents. If you genuinely need a read-only role that can see Secrets, that is a separate, deliberate grant.- ServiceAccount RBAC is the bigger attack surface. Humans come and go; ServiceAccount tokens live in Pods that may be compromised. The blast radius of a compromised Pod is exactly its ServiceAccount’s RBAC. Scope ServiceAccount roles tightest of all.
- GitOps your RBAC. RBAC objects belong in version control and flow through GitOps like any other manifest —
kubectl auth reconcile -fapplies them while preserving additive semantics (it merges rather than replaces). Imperativekubectl create rolebindingis for emergencies. - Default bindings are part of the cluster.
kubectl get clusterrolebindingson a fresh cluster shows ~50system:*bindings — that is normal and load-bearing. The audit target is the non-system:bindings you and your tooling added.
See Also
- Kubernetes Authorization Modes — RBAC is one mode in the authorizer chain; explains how it composes with Node/Webhook
- Kubernetes Authentication — produces the Username/Groups that RBAC bindings name as subjects
- ServiceAccount — the in-cluster identity most often bound by RBAC;
system:serviceaccount:*subjects - API Server Request Flow — RBAC runs in the authorization stage of this pipeline
- ValidatingAdmissionPolicy / OPA Gatekeeper / Kyverno — where genuine deny policy lives
- Namespace — the scope boundary that
RoleBindingand namespacedRoleoperate within - kube-apiserver — the component running the RBAC authorizer
- Kubernetes Audit Logging — records which RBAC decision authorized each request
- Kubernetes MOC — parent MOC (§12 Security)