Where RBAC sits
The problem. Everyone who can reach the apiserver as cluster-admin can delete production. Developers need to read their own namespace, a monitoring agent needs to list pods, a CI job needs to update one Deployment - and nothing more. RBAC (Role-Based Access Control) is how Kubernetes says who may do what.
What you need to know already: the apiserver and etcd (15.5), namespaces (15.26), kubeconfig (15.1), Linux users and groups as the same idea on one machine (4.3), TLS certificates and their CN (9.15, 9.17), admission (17.9).
Two words that sound alike: authentication = proving who you are; authorization = deciding what you may do. Every request to the apiserver goes authentication -> authorization -> admission -> validation -> etcd. Authentication answers who are you (a client certificate, a bearer token - a secret string sent with each request - or an OIDC token: a signed login token from a company login system). RBAC is the authorizer: it answers may this identity perform this verb (action: get, list, create, delete...) on this resource (kind of object: pods, secrets...) in this namespace. It sees exactly four things about a request:
user: system:serviceaccount:dev:app groups: [system:serviceaccounts system:serviceaccounts:dev system:authenticated]
verb: list
resource: pods (API group "", subresource none, name none)
namespace: dev
The refusal message is those four things, printed back:
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev:app" cannot list resource "pods" in API group "" in the namespace "dev"
Learn to read it as a spec: who (User), verb (list), resource (pods), API group ("" = core), where (namespace dev, or "at the cluster scope"). Everything you need to write the missing rule is in that sentence.
Subjects: who can be granted anything
A subject is whoever receives permissions:
- User - a name string. Kubernetes has no User objects. Users come from the authenticator: the CN (common name) of a client certificate, the
sub/emailfield (a "claim") of an OIDC token.kubectl get usersdoes not exist. - Group - also just a string from the authenticator (certificate O=, OIDC groups claim), plus built-ins:
system:authenticated,system:unauthenticated,system:masters(bypasses RBAC entirely - kubeadm's old admin group),system:serviceaccounts,system:serviceaccounts:<ns>. - ServiceAccount - a real, namespaced API object, the identity of a workload (a program running in a pod, lesson 17.33). Its user name is
system:serviceaccount:<namespace>:<name>.
Your kubernetes-admin kubeconfig user is a certificate with CN=kubernetes-admin and O=kubeadm:cluster-admins, and a ClusterRoleBinding kubeadm:cluster-admins binds that group to cluster-admin:
$ kubectl auth whoami
ATTRIBUTE VALUE
Username kubernetes-admin
Groups [kubeadm:cluster-admins system:authenticated]
kubectl auth whoami asks the apiserver who it thinks you are.
Roles: what may be done
A Role is a list of rules ("these verbs on these resources"). It grants nothing until a binding connects it to a subject (next section).
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # namespaced: only meaningful in its own namespace
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""] # "" = core (pods, services, configmaps, secrets...)
resources: ["pods", "pods/log"] # subresources are separate: pods/log, pods/exec, deployments/scale
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"] # only this object
verbs: ["get"]
- apiGroups: the API group of the resource (Kubernetes files its kinds into groups; the original ones are the "core" group, written
"") -kubectl api-resourcesshows it (deployments->apps,ingresses->networking.k8s.io,cronjobs->batch). The most common RBAC bug is the wrong group: a rule fordeploymentswithapiGroups: [""]grants nothing. - verbs:
get list watch create update patch delete deletecollection, plus special ones:impersonate,bind,escalate,use,approve.*= all. - resourceNames restrict to named objects - but only for verbs that name an object (get, update, patch, delete).
list,watchandcreatecannot be restricted by name. - A ClusterRole has the same rules but no namespace. It is needed for cluster-scoped resources (nodes, persistentvolumes, namespaces, clusterroles...), for non-resource URLs (
/healthz,/metrics), and for roles you want to reuse in many namespaces.
Generate, do not type (and add $do - the speed-kit variable for --dry-run=client -o yaml, 15.3 - to see the YAML). kubectl create role NAME --verb=... --resource=... builds a Role from flags:
$ kubectl create ns dev
namespace/dev created
$ kubectl create role pod-reader -n dev --verb=get,list,watch --resource=pods,pods/log,deployments
role.rbac.authorization.k8s.io/pod-reader created
$ kubectl describe role pod-reader -n dev
Name: pod-reader
Labels: <none>
Annotations: <none>
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
pods [] [] [get list watch]
pods/log [] [] [get list watch]
deployments.apps [] [] [get list watch]
kubectl resolved deployments to the apps group itself - one reason to prefer the generator. deployments.apps in the table is resource.group.
Bindings: who gets which role, and where
A RoleBinding connects a Role (or ClusterRole) to subjects inside one namespace; a ClusterRoleBinding does it cluster-wide. --role/--clusterrole = which role, --user, --group, --serviceaccount=NAMESPACE:NAME = who:
$ kubectl create rolebinding jane-reads-pods -n dev --role=pod-reader --user=jane
$ kubectl create rolebinding app-reads-pods -n dev --role=pod-reader --serviceaccount=dev:app
$ kubectl create clusterrolebinding oncall-view --clusterrole=view --group=oncall
The four combinations, and the one everyone gets wrong:
| binding | role | effect |
|---|---|---|
| RoleBinding in ns X | Role in ns X | the rules, in namespace X |
| RoleBinding in ns X | ClusterRole | the ClusterRole's rules, only in namespace X |
| ClusterRoleBinding | ClusterRole | the rules in every namespace, plus cluster-scoped resources |
| ClusterRoleBinding | Role | not allowed (roleRef.kind must be ClusterRole) |
A RoleBinding can reference a ClusterRole, but the grant stays namespaced. This is how you reuse one definition: bind the built-in
viewClusterRole with a RoleBinding indevand the user can viewdev- notprod, and not nodes (nodes are cluster- scoped; a namespaced binding can never grant them).
roleRef (the binding's "which role" field) is immutable (cannot be changed) - to point a binding at another role, delete and recreate it:
The RoleBinding "jane-reads-pods" is invalid: roleRef: Invalid value: rbac.RoleRef{APIGroup:"rbac.authorization.k8s.io", Kind:"Role", Name:"pod-admin"}: cannot change roleRef
A binding may reference a role that does not exist yet (it grants nothing until the role appears) - RBAC objects can be applied in any order.
RBAC is purely additive
There are no deny rules. The authorizer walks every ClusterRoleBinding and every RoleBinding in the request's namespace; if any rule in any bound role allows the request, it is allowed. Consequences:
- You cannot "take away" delete from someone who has
editby adding another role. You remove the binding that grants it. - "Who can delete pods in prod?" is answered by looking at every binding, not one role.
- Least privilege = few, small bindings.
Reading means reading: list and watch on secrets
Say jane asks to see which secrets exist in dev, and someone grants exactly that - list, nothing else:
$ kubectl create secret generic db-creds -n dev --from-literal=password=s3cr3t-p-w0rd
secret/db-creds created
$ kubectl create role secret-lister -n dev --verb=list --resource=secrets
role.rbac.authorization.k8s.io/secret-lister created
$ kubectl create rolebinding jane-lists-secrets -n dev --role=secret-lister --user=jane
rolebinding.rbac.authorization.k8s.io/jane-lists-secrets created
--as=jane = impersonation: send the request as if you were jane (cluster-admin may do this), the quickest way to test someone else's permissions:
$ kubectl get secret db-creds -n dev --as=jane
Error from server (Forbidden): secrets "db-creds" is forbidden: User "jane" cannot get resource "secrets" in API group "" in the namespace "dev"
$ kubectl get secrets -n dev -o yaml --as=jane
apiVersion: v1
items:
- apiVersion: v1
data:
password: czNjcjN0LXAtdzByZA==
...
jane has only list on secrets, and get is refused. But a list response contains the full objects, data included: list (and watch) on secrets is read access to every secret in the namespace. The same for configmaps with credentials in them.
The default roles
$ kubectl get clusterroles | grep -v system:
NAME CREATED AT
admin 2026-09-11T08:46:31Z
cluster-admin 2026-09-11T08:46:31Z
edit 2026-09-11T08:46:31Z
view 2026-09-11T08:46:31Z
...
- view - read most namespaced objects, not secrets, not roles/bindings.
- edit - view + create/update/delete workloads, services, configmaps, secrets (read and write), exec into pods. Not roles or bindings.
- admin - edit + Roles and RoleBindings in the namespace (a namespace owner).
- cluster-admin - everything, everywhere (
*on*, plus non-resource URLs).
admin, edit and view are aggregated ClusterRoles: they carry aggregationRule: {clusterRoleSelectors: [{matchLabels: {rbac.authorization.k8s.io/aggregate-to-view: "true"}}]}, and any ClusterRole you label aggregate-to-view: "true" (the read rules for a CRD - a CustomResourceDefinition, i.e. a new object kind someone added to the cluster - say) is merged into view automatically by a controller.
Escalation prevention
You cannot grant what you do not have. Creating a Role with verbs you lack, or binding a role more powerful than yourself, is refused unless you hold the special verbs escalate (on roles) or bind (on the role being bound). That is why a namespace admin can hand out edit and view in their namespace but cannot mint cluster-admin.
What you can now do
- Read a Forbidden message as a spec (who, verb, resource, group, namespace).
- Create Roles, ClusterRoles and bindings with
kubectl create, and pick the right combination. - Explain why RBAC has no deny, and why
liston secrets means read.