Gendi Notes

Kubernetes RBAC

Four object types and one rule: permissions are **purely additive**, there is no deny. If someone can do something they should not, you remove a binding — you cannot add a rule to stop them.

Level 3 guide kubernetesrbacsecurity

What is it?#

RBAC answers the authorization question — the second gate in the API server, after authentication has established who you are.

request ──→ ① AUTHN "you are alice@example.com" │ ↓ ② AUTHZ "may alice CREATE PODS in namespace prod?" ← RBAC │ checks every Role bound to her. │ ANY rule that allows it → allowed. │ No rule allows it → 403. ↓ ③ ADMISSION → ④ VALIDATION → etcd

What it is made of#

Four objects, and the only real complexity is the two-by-two grid.

ObjectDefinesScope
RoleWhat may be doneOne namespace
ClusterRoleWhat may be doneCluster-wide, or cluster-scoped resources
RoleBindingWho gets a roleOne namespace
ClusterRoleBindingWho gets a roleCluster-wide
Role (namespaced) ClusterRole (cluster-wide) ───────────────── ────────────────────────── RoleBinding permissions in permissions in THAT ONE (namespaced) that namespace namespace only ← the useful combination ClusterRole ✘ not allowed permissions in EVERY Binding (cannot widen a namespace, plus cluster- (cluster-wide) namespaced Role) scoped resources ← the dangerous one

How to use it#

A Role and a binding#

Read-only access to pods and their logs, in one namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: pod-reader
rules:
  - apiGroups: [""]                 # "" is the CORE group: pods, services...
    resources: ["pods", "pods/log"] # subresources are separate. This matters.
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: production
  name: devs-read-pods
subjects:
  - kind: User
    name: alice@example.com         # from your identity provider
    apiGroup: rbac.authorization.k8s.io
  - kind: ServiceAccount
    name: ci-deployer
    namespace: ci                   # a SA from ANOTHER namespace is allowed
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
FieldGotcha
apiGroups: [""]The core group. Pods, Services, ConfigMaps, Secrets, Nodes
resourcesPlural, lowercase: pods not Pod
pods/log, pods/execSubresources are separate permissions
verbsget list watch create update patch delete deletecollection
kind: UserKubernetes has no user objects — these come from your IdP

ServiceAccounts — identity for pods#

Every pod runs as a ServiceAccount. If you do not name one, it is default.

spec:
  serviceAccountName: my-app
  automountServiceAccountToken: false     # if the app never calls the API
kubectl create serviceaccount my-app -n prod
kubectl create rolebinding my-app-reader \
  --role=pod-reader --serviceaccount=prod:my-app -n prod

Testing permissions — the commands that actually answer the question#

# Can I?
kubectl auth can-i create deployments -n prod

# Can SOMEONE ELSE? (needs impersonation rights)
kubectl auth can-i delete pods -n prod --as=alice@example.com
kubectl auth can-i '*' '*' --as=system:serviceaccount:prod:default

# Everything a subject can do — the review command
kubectl auth can-i --list --as=system:serviceaccount:prod:my-app -n prod

# Who has cluster-admin? Run this on your cluster today.
kubectl get clusterrolebindings -o json | jq -r '
  .items[] | select(.roleRef.name=="cluster-admin")
  | .metadata.name + " -> " + ([.subjects[]?.name] | join(", "))'

What goes wrong#

SymptomCauseFix
403 forbidden and the message names the missing verbNo rule allows itkubectl auth can-i --list; add a rule
Read-only dashboard shows nothingget without listGrant get, list, watch
cannot get resource "pods/log"Subresource not grantedAdd pods/log explicitly
Binding exists but has no effectWrong namespace, or a typo in roleRefroleRef is immutable — delete and recreate
Works for you, fails in CIYou are admin; the ServiceAccount is not--as=system:serviceaccount:<ns>:<name>
attempt to grant extra privilegesYou cannot grant what you do not haveEscalation prevention — see below

Interview corner · الأسئلة اللي بتتسأل#

What is the difference between a Role and a ClusterRole, and when do you combine them?

Role is namespaced; ClusterRole is cluster-wide. The binding determines the effective scope, which is where the useful combination lives.

Role kindBinding kindEffective scope
RoleRoleBindingThat one namespace
ClusterRoleRoleBindingThat one namespace — the pattern you want
ClusterRoleClusterRoleBindingEvery namespace, plus cluster-scoped resources
RoleClusterRoleBindingImpossible

Use a ClusterRole with a RoleBinding to define a permission set once and grant it per namespace — that is how the built-in view, edit and admin roles are meant to be used.

You need a ClusterRole with a ClusterRoleBinding for genuinely cluster-scoped resources: nodes, PersistentVolumes, namespaces, CustomResourceDefinitions. Those have no namespace, so a RoleBinding cannot express permission over them.

How would you debug a 403 Forbidden from a pod's application?

The pod acts as its ServiceAccount, so reproduce it as that identity rather than as yourself — that is the whole trick.

# 1. Which ServiceAccount is it actually using?
kubectl get pod <pod> -n prod -o jsonpath='{.spec.serviceAccountName}{"\n"}'

# 2. Ask the API server directly, as that identity
kubectl auth can-i list configmaps \
  --as=system:serviceaccount:prod:my-app -n prod

# 3. Everything it CAN do — usually the fastest way to spot the gap
kubectl auth can-i --list --as=system:serviceaccount:prod:my-app -n prod

# 4. Which bindings actually reference it?
kubectl get rolebindings,clusterrolebindings -A -o json | jq -r '
  .items[] | select(.subjects[]?.name=="my-app") | .metadata.name'

Read the 403 message itself — Kubernetes names the verb, resource, API group and namespace it wanted. That usually identifies the missing rule immediately.

Common causes, in order: wrong namespace in the RoleBinding; a missing subresource like pods/log; get granted without list; the pod using default because serviceAccountName was never set.

There is no "deny" rule in RBAC. What are the consequences?

RBAC is purely additive: the API server evaluates every rule bound to the subject, and allows the request if any rule permits it. There is no way to subtract.

Four practical consequences:

  1. To remove access you remove a binding, not add a rule. So you must be able to find every binding that grants it — which is why broad ClusterRoleBindings are dangerous: they are easy to add and hard to audit.
  2. A wildcard is unrecoverable except by deletion. verbs: ["*"] on resources: ["*"] cannot be narrowed by a second rule.
  3. Auditing means enumerating, so kubectl auth can-i --list --as=... is the only reliable answer to "what can this identity do?" Reading individual Roles will mislead you.
  4. For real deny semantics you need admission control — a validating webhook, OPA Gatekeeper or Kyverno — because that stage can reject. RBAC answers "may you", admission answers "should this specific object be allowed".

Key takeaways#

  • Permissions are additive; there is no deny. Remove bindings, do not add rules.
  • A RoleBinding referencing a ClusterRole is the pattern: define once, grant per namespace.
  • get does not imply list, and list returns full objects — so list on secrets is get on all of them.
  • Subresources are separate: pods/log, pods/exec, pods/portforward.
  • Test as the failing identity with --as=, never as yourself.
  • automountServiceAccountToken: false for apps that never call the API.
  • Audit cluster-admin bindings — a ServiceAccount with it turns any code execution into cluster compromise.
  • Nuanced policy belongs in admission control, not RBAC.

Last updated 2026-09-13