Alright, let’s get down to the brass tacks of actually granting those permissions we so carefully defined in our Roles and ClusterRoles. Think of those objects as the menu of possible powers—detailed, but utterly useless just sitting there. A Role by itself does precisely nothing. It’s a recipe with no chef, a concert ticket with no venue. To make the magic happen, you need to bind that menu of powers to a user, a group, or (most commonly) a ServiceAccount. That’s the job of the RoleBinding and ClusterRoleBinding resources. They are the bouncers that look at your ID and say, “Alright, you’re on the list, come on in.”

The Crucial Difference: Binding vs. ClusterBinding

This is the single most important concept to grasp, and if you get it wrong, you’ll be tearing your hair out wondering why your perfectly crafted permissions are having no effect.

A RoleBinding grants permissions within a specific namespace. Here’s the kicker: it can reference either a Role (which is namespaced) or a ClusterRole (which is not). When you use a RoleBinding to reference a ClusterRole, the permissions granted are still limited to the namespace of the RoleBinding. It’s a way to use a common, predefined set of permissions (the ClusterRole) across many namespaces without having to redefine it every time.

A ClusterRoleBinding, on the other hand, grants permissions across the entire cluster (i.e., cluster-scoped). It must reference a ClusterRole. You cannot use a regular Role here. Use these with extreme prejudice. Granting someone cluster-admin via a ClusterRoleBinding is effectively giving them the keys to the entire kingdom. There is no namespace that can hide from it.

Let’s make this concrete. Imagine you have a ClusterRole named pod-reader that allows get, list, and watch on pods.

# clusterrole-pod-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]

Now, see the difference in how we bind it:

Binding it to a user only in the testing namespace:

# rolebinding-in-namespace.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: testing # This is the critical line
subjects:
- kind: User
  name: jane-doe
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole # Notice we're referencing a ClusterRole!
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Jane can only read pods in the testing namespace. She can’t even see pods in production.

Binding it to a group across the entire cluster:

# clusterrolebinding-global.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-pods-global
subjects:
- kind: Group
  name: pod-auditors
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Anyone in the pod-auditors group can now read pods in every single namespace in the cluster. See why we’re cautious with these?

Anatomy of a Binding: subjects and roleRef

The structure of a Binding is simple but must be perfect. You’ll mess this up at least once, I guarantee it.

  • subjects: This is the list of who gets the power. It can be Users, Groups, or ServiceAccounts. The kind and name are mandatory. For ServiceAccounts, you must also specify the namespace they belong to, because unlike users, SAs are namespaced creatures. For users and groups, you omit the namespace field. The system for managing those actual users and groups is handled outside Kubernetes (e.g., by your cloud provider or via certificates), which is a whole other rant about Kubernetes’… let’s call it “modular” design.

  • roleRef: This points to the Role or ClusterRole you’re granting. This field is immutable. You cannot change it after creation. This is the part everyone tries to edit and then gets frustrated. The roleRef is a direct, hard link. You can’t use a label selector to point to a role; you must state its kind (Role or ClusterRole) and its name explicitly. If you need to change it, you create a new Binding.

The Classic Pitfall: The Missing ServiceAccount Namespace

This is the number one “why doesn’t this work!?” issue. You create a RoleBinding in namespace: app to grant permissions to a ServiceAccount. But you forget to tell the binding which namespace the ServiceAccount lives in. The binding assumes the SA is in the same namespace as itself, which is often not true (e.g., you might have a central namespace for SAs).

WRONG:

subjects:
- kind: ServiceAccount
  name: my-app-sa # This SA is in the 'core-services' namespace, not 'app'!
  # namespace: core-services  <-- You absolute fool, you forgot this line!

RIGHT:

subjects:
- kind: ServiceAccount
  name: my-app-sa
  namespace: core-services # The binding now knows exactly where to find the SA.

Without that namespace field, Kubernetes is fruitlessly looking for a ServiceAccount named my-app-sa in the app namespace. It won’t find it, and your Pod using my-app-sa will get a heartbreaking “permission denied”.

Best Practices from the Trenches

  1. Prefer RoleBindings + ClusterRoles over RoleBindings + Roles: This is my strong opinion. Define your permission sets as ClusterRoles (company:pod-reader, company:configmap-editor). Then, use RoleBindings in each namespace to grant those standardized roles to subjects. This is infinitely more maintainable than having hundreds of identical Roles scattered across namespaces. You have one source of truth for what “pod-reader” means.

  2. ClusterRoleBindings are for Cluster Admins Only: Seriously. The list of valid use cases is very short: cluster admins, nodes, volume provisioners, and controllers that need cluster-scoped access. If you’re about to create one, stop and ask yourself if the subject truly needs access to every single object of that type in the cluster. The answer is usually no.

  3. Use Groups, Not Individual Users: Bind permissions to groups (kind: Group) and then manage membership in that group in your external identity provider (e.g., Azure AD, GitHub Teams, OpenID Connect). This keeps your RBAC manifests clean and declarative, describing roles rather than a constantly shifting list of people.

  4. The system:serviceaccount: Prefix is a Trap: You might see examples binding to system:serviceaccount:default:default. This is the fully qualified name for the default ServiceAccount in the default namespace. Do not use this. It’s an anti-pattern. Always use the kind: ServiceAccount method shown above. It’s clearer and less error-prone. The old prefix style is a relic.