Kubernetes RBAC Generator
Build a Role or ClusterRole with its binding, and see what it actually grants. The output runs through our own RBAC analyzer as you type, so picking an escalating verb reports the escalation path here rather than in your cluster.
rbac.yaml
updates as you type Common mistakes
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Using a ClusterRoleBinding where a RoleBinding was meant
A ClusterRoleBinding grants the role in every namespace. The difference is one word and the blast radius is the whole cluster.
Instead:RoleBinding for namespace scope.
Granting list on secrets
get on a named secret is narrow. list returns the contents of every secret in the response, which is a very different grant.
Instead:Avoid list on secrets. Use get with resourceNames.
Including a wildcard verb
It covers delete, deletecollection, escalate, bind and impersonate. Impersonate in particular is privilege escalation.
Instead:List the verbs you need.
Reading an RBAC rule for what it actually grants
RBAC is purely additive. There are no deny rules, no ordering and no precedence: a subject can do something if any binding says so. That makes each rule easy to read and the total effect hard to see, which is where the surprises live.
The core group is the empty string, and "core" grants nothing
Pods, Services, Secrets, ConfigMaps and Nodes are all in the group with no name. In YAML that is two quote characters. Writing core, or v1, or omitting the field is not an error: the API server stores the rule and it matches nothing, so the role grants no access and reports no problem. This is the most common reason a Role that looks right does not work.
rules:
- apiGroups: [""] core: pods, services,
secrets, configmaps
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: ["apps"] deployments, statefulsets
- apiGroups: ["core"] matches NOTHING
- apiGroups: ["v1"] also matches nothing A ClusterRole bound by a RoleBinding is namespaced
The role's scope comes from the binding, not from the role. This is the intended way to define a permission set once and apply it in many namespaces, and because it reads like a mistake, people write near-identical Roles in every namespace instead. The reverse does not work: a Role cannot be referenced by a ClusterRoleBinding at all.
ClusterRole + ClusterRoleBinding everywhere
ClusterRole + RoleBinding one namespace
Role + RoleBinding one namespace
Role + ClusterRoleBinding rejected
# and there is no way to narrow a ClusterRoleBinding
# afterwards. It has no namespace field. Three verbs that are cluster-admin with extra steps
escalate, bind and impersonate each let the holder end up with more than the role appears to grant, and none of them show up when you audit who is bound to cluster-admin. They exist for legitimate reasons, and a role carrying one should be a deliberate decision rather than something copied from a controller's manifest.
escalate on roles/clusterroles:
write yourself a cluster-admin role
bind on roles/clusterroles:
bind yourself to cluster-admin without
holding those permissions first
impersonate on users/groups:
kubectl --as=... anyone, including
system:masters Reading Secrets is reading credentials
A ServiceAccount's token is a Secret, or is obtainable through the TokenRequest API by anything that can create pods. So get or list on secrets in a namespace is the union of what every ServiceAccount in that namespace can do, and create on pods is at least the most privileged ServiceAccount there. Neither reads like an escalation in the rule.
resources: ["secrets"]
verbs: ["get", "list"]
-> every ServiceAccount token in the namespace
-> and therefore everything they can each do
resources: ["pods"]
verbs: ["create"]
-> mount any ServiceAccount in the namespace
-> without a policy engine, also hostPath the node resourceNames does not restrict list or watch
It looks like the way to scope a rule to one object, and for get, update, patch and delete it is. For list and watch the name is not known until the response has already been assembled, so the API server cannot filter by it and grants the verb over everything of that type. The rule reads as restricted and is not.
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["my-config"]
verbs: ["get", "list"]
get -> only my-config restricted
list -> every configmap NOT restricted
# split them: resourceNames with get/update/delete in
# one rule, and no list verb at all.