20.6 Least-Privilege RBAC Design
Right, let’s talk about designing RBAC with the principle of least privilege. This is where the rubber meets the road. It’s also where most people screw it up royally, creating either a security nightmare or a usability brick wall. The goal is simple: give an identity—a user, a service account, a pod—only the permissions it absolutely needs to do its job and not a single one more. It sounds obvious, but you’d be amazed how often this is handled with the subtlety of a sledgehammer.
The biggest mistake I see? The “just give it the admin role” approach. You’re not lazy, you’re busy. I get it. But that one decision is the digital equivalent of leaving the keys in the ignition of a running car. We’re better than that.
Start by Granting a Role, Not a User
This is RBAC 101, but it’s shocking how many folks bind a role directly to a user. Don’t. You use a RoleBinding (or ClusterRoleBinding). This creates a crucial layer of abstraction. The user is assigned a role, and the role has the permissions. Want to change what a user can do? You change the role they’re bound to, not the user definition itself. This is clean, scalable, and sane.
Here’s the wrong way. Please don’t do this. I’m showing you so you know the enemy.
# This is a crime. Do not replicate.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: bad-practice-binding
subjects:
- kind: User
name: jane.doe@example.com # Directly binding a user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin # And giving them the keys to the kingdom
apiGroup: rbac.authorization.k8s.io
Instead, you bind users to groups (which you manage in your identity provider, like Okta or Azure AD), and you bind the group to a role. Kubernetes doesn’t manage groups; it trusts the subjects you put in the binding. This is the way.
# This is the way.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: sensible-binding
subjects:
- kind: Group
name: platform-engineers # A group from your IdP
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin # A custom role you've defined, NOT cluster-admin
apiGroup: rbac.authorization.k8s.io
The Art of the Custom Role
The built-in roles like admin, edit, and view are, to be frank, garbage for least privilege. They’re wildly over-permissioned. Their real purpose is for bootstrapping and maybe, maybe, for a developer’s local minikube cluster. For any real environment, you must create your own custom ClusterRole definitions.
Let’s say you have a CI/CD system that needs to deploy pods to a specific namespace. What does it actually need? It needs to create, update, and get workloads (Pods, Deployments, etc.), and it might need to manage Services and Ingresses. It does not need to read Secrets (unless specifically required), list all Nodes, or evict Pods from the entire cluster.
Here’s a realistic, least-privilege role for a CI/CD bot:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cicd-deployer
rules:
- apiGroups: [""] # Core API group
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps", "extensions"]
resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Notice the absence of 'secrets', 'nodes', 'persistentvolumes', etc.
This role is then bound to a specific service account in a specific namespace using a RoleBinding, locking its power down to that one namespace.
The Power and Peril of Verb Wildcards
Be terrified of the wildcard verb: "*". It’s a code smell. It means you probably haven’t thought through what this identity actually needs to do. Does your application need to delete a Pod? Or does it just need to get and list? Granting verbs: ["*"] on resources: ["pods"] is how a minor bug in your app turns into a pod-killing spree.
Always enumerate the verbs explicitly. It’s more typing, but it’s the difference between a scalpel and a broadsword.
# Bad: Lazy and dangerous
verbs: ["*"]
# Good: Explicit and secure
verbs: ["get", "list", "watch"]
The ResourceNames Wildcard
Here’s a pro move that most people miss. You can restrict a role to only have access to a specific instance of a resource by name. This is least privilege on steroids.
Imagine you have a CI/CD system that needs to update a single, specific ConfigMap that holds its configuration. You don’t need to give it access to all ConfigMaps in the namespace. You can lock it down to just that one.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: my-app
name: configmap-updater
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["cicd-configuration"] # The ONE configmap it can touch
verbs: ["get", "update"]
This is incredibly powerful for creating super granular, secure roles. Use it.
The Two-Amphora Problem: Testing Your Roles
You’ve crafted the perfect, minimal role. How do you know it actually works? You can’t just assume. You have to test. The kubectl auth can-i command is your best friend here.
Don’t just test if the role can do what it’s supposed to; test if it can’t do what it’s not supposed to. This is the only way to be sure.
# Test if the service account can do what it needs
kubectl auth can-i create deployment --as=system:serviceaccount:my-app:ci-bot -n my-app
> yes
# Now test if it can do something it should NOT be able to do
kubectl auth can-i delete secret --as=system:serviceaccount:my-app:ci-bot -n my-app
> no # This 'no' is what we want to see. Celebrate the 'no's.
# Also test in other namespaces to ensure it's contained
kubectl auth can-i list pods --as=system:serviceaccount:my-app:ci-bot -n other-namespace
> no # Another beautiful, beautiful 'no'.
This process is manual, but it’s non-negotiable. For critical roles, script it. Make it part of your CI/CD. The few minutes it takes will save you from the catastrophic incident later. Trust me, I’ve learned this the hard way so you don’t have to.