Understanding Kubernetes: RBAC Without Losing Your Mind

Understanding Kubernetes: RBAC Without Losing Your Mind

In the previous articles we looked at different parts of Kubernetes networking: how Pods communicate, how Services provide stable access, and how Network Policies can restrict traffic.

Today we want to answer a slightly different question: Just because something can access a resource, should it? Let’s say a frontend application might need to talk to a backend service. But should it also be able to read Secrets? Or should every developer in the team be able to delete Deployments in production?

This is where RBAC comes in - Role-Based Access Control. It allows us to define who can do what and where. And once you understand the basic building blocks, it is actually not as scary as it might look at first. 😉

The basic idea behind RBAC

RBAC answers three simple questions:

  • Who is trying to do something?
  • What do they want to do?
  • Where should this be allowed?

For example:

“Allow the frontend application (who) to read ConfigMaps (what) in the production namespace (where).”

RBAC does not directly assign permissions to users or applications. Instead, we create reusable permission definitions and then connect them to identities.

To work effectively with RBAC, there are four main objects we need to understand:

  • Role
  • RoleBinding
  • ClusterRole
  • ClusterRoleBinding

Let’s start with the smallest scope.

Roles: Permissions inside a namespace

A Role defines permissions within a single namespace.

For example, we want to allow someone to read Pods in the development namespace. A Role could look like this:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: development
rules:
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - get
      - list

The important part here is the rule

resources:
  - pods

verbs:
  - get
  - list

saying “Someone with this Role can get and list Pods.” But the Role itself does not define who gets this permission. For that, we need a Binding.

RoleBindings connect permissions to identities

A RoleBinding assigns a Role to someone, for example:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: development
subjects:
  - kind: User
    name: alice
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

allows Alice to read Pods in the development namespace. But she cannot delete Pods, access Secrets or read Pods in other namespaces, because we did not grant these permissions. This is also the basic RBAC principle: Only grant the permissions that are actually needed.

ClusterRoles: permissions beyond namespaces

Sometimes permissions should not be limited to a single namespace, e.g. for viewing Nodes, managing namespaces or accessing cluster-wide resources. For this, Kubernetes provides ClusterRoles.

A ClusterRole looks very similar to a Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
      - list

But the difference is the scope: While a Role lives inside a namespace, a ClusterRole applies cluster-wide (as the name already suggests). To assign a ClusterRole, we then use a ClusterRoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: node-reader-binding
subjects:
  - kind: User
    name: alice
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io

This now allows Alice to read Nodes across the whole cluster.

Users vs ServiceAccounts

One thing that often causes confusion: Who exactly is the “user” in Kubernetes?

There are two important types of identities: Users are usually humans, e.g. developers, administrators, or operators, like Alice in our example from above. ServiceAccounts are identities for workloads running inside Kubernetes.

Imagine an application that needs to read ConfigMaps. The application itself is not a person, so we create a ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: production

Then we can bind permissions to this ServiceAccount. This is how many controllers and Kubernetes operators work internally.

Verbs, resources and API groups

When writing RBAC rules, you will often find these three fields:

  • Verbs describe which actions are allowed, e.g. get, list, create, update, or delete
  • Resources are e.g. Pods, Deployments, Services, or Secrets, so Kubernetes objects we want to access.
  • API Groups describe where the resource belongs to. This is where it gets a bit trickier: we need to reference the correct API group.

Pods, for example, are part of the core API, whereas Deployments are part of the apps API group - and hence both get referenced differently:

# Pods:
apiGroups:
  - ""

# Deployments:
apiGroups:
  - apps

This is why RBAC rules sometimes look a bit confusing in the beginning.

If you are unsure which API group or resource name to use, kubectl api-resources is your friend. It will show you the available resources and their API groups.

Be aware: RBAC is not automatically secure

Kubernetes comes with some predefined ClusterRoles like cluster-admin. Be careful when using these, because they often grant far more permissions than a workload actually needs.

Demo time

Let’s try this out in a small example. First, we create a namespace via

kubectl create namespace demo

and then a ServiceAccount:

kubectl create serviceaccount pod-reader \
  --namespace demo

Next, we create a Role that allows reading Pods

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: demo
rules:
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - get
      - list

and apply it via

kubectl apply -f role.yaml

Now we connect it to the ServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-binding
  namespace: demo
subjects:
  - kind: ServiceAccount
    name: pod-reader
    namespace: demo
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

We apply again

kubectl apply -f rolebinding.yaml

so we can test our permissions. We don’t need to create a Pod for this demo. Instead, kubectl auth can-i allows us to test permissions by impersonating this ServiceAccount.

kubectl auth can-i get pods \
  --namespace demo \
  --as=system:serviceaccount:demo:pod-reader

The answer should be yes. Worked? Great, we can read Pods. Let’s try now if we can delete pods via

kubectl auth can-i delete pods \
  --namespace demo \
  --as=system:serviceaccount:demo:pod-reader

Got no? Exactly what we wanted. :)

Summing up

RBAC is Kubernetes’ way of controlling access - and one of the foundations of securing clusters. The core idea is simple: Roles define permissions, Bindings assign permissions, and ServiceAccounts give applications an identity.

Together with Network Policies, Secrets management, and Pod Security, it helps us move from a very open default setup to a more controlled and secure environment. If you want to dig deeper into Secrets management, have a look at my article about ConfigMaps & Secrets I wrote a while back. Next up in this series: Pod Security. Stay tuned :)

header image created by buddy ChatGPT