RBAC minimization

Least privilege and the grants that are really escalation.

Advanced14 min · lesson 7 of 24

Once a request is authenticated, RBAC decides whether it is allowed — and it is the control that most often drifts toward over-permission, because granting access fixes a broken deployment in seconds and nobody ever comes back to tighten it. RBAC is purely additive and default-deny: nothing is permitted unless some binding grants it, and there are no deny rules, so minimization is a subtraction game played against grants that quietly accumulated.

Four objects, two pairs. A Role (namespaced) or ClusterRole (cluster-wide) holds rules — combinations of apiGroups, resources, and verbs. A RoleBinding or ClusterRoleBinding attaches a role to subjects: users, groups, or service accounts. The useful trick is that a RoleBinding can reference a ClusterRole, reusing one role definition namespace-by-namespace without granting it cluster-wide.

How an RBAC decision is made
1subjectuser / group / SA2bindinglinks subject to role3roleapiGroups × resources ×…4allow / denydefault: deny
Role vs ClusterRole = where the permission is defined. RoleBinding vs ClusterRoleBinding = where the grant applies. Everything is additive; the union of all matching rules is what the subject can do.
role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # namespaced
metadata: { namespace: payments, name: deployer }
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"] # named verbs, never ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { namespace: payments, name: ci-deployer }
subjects:
- kind: ServiceAccount
name: deployer
namespace: ci
roleRef: { kind: Role, name: deployer, apiGroup: rbac.authorization.k8s.io }

Audit what already exists

Start by finding the over-grants. List everyone bound to cluster-admin, hunt for wildcard verbs or resources, and use kubectl auth can-i — the authoritative query, because it runs the real authorizer — to ask what a given subject can actually do, impersonating it with --as.

terminal
# every subject bound to cluster-admin
$ kubectl get clusterrolebindings -o json | jq -r '
.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?.name'
# what can this service account really do, and can it do the scary thing?
$ kubectl auth can-i --list --as=system:serviceaccount:ci:deployer -n payments
$ kubectl auth can-i create pods --as=system:serviceaccount:ci:deployer -n kube-system
no

The grants that are really escalation

Some permissions are far more dangerous than they read because they let a subject grant themselves more, or reach past the namespace. secrets:get reads every credential in scope. pods/exec is an interactive shell into any pod. create pods lets a subject mount any service account (including a powerful one) and inherit its token. And the verbs escalate, bind, and impersonate are literally self-promotion. Treat all of these as near-admin.

Least-privilege review checklist
delete on sight
verbs: ["*"]
name the verbs you need
resources: ["*"]
name the resources
cluster-admin binding
to a workload SA
grant sparingly, scoped
secrets get/list
reads all credentials
pods/exec · create pods
shell / mount any SA
escalate / bind / impersonate
self-promotion
A namespaced Role beats a ClusterRole every time you can manage it. The wildcard is not a convenience; it is how a workload SA quietly ends up able to read every secret in the cluster.
Verify the tightening, do not assume it
After you scope a role down, re-run kubectl auth can-i as the subject against the thing it should no longer be able to do. The answer is authoritative because it runs the same authorizer the API server uses — “I removed the wildcard” is a hope; “can-i create secrets returns no” is a fact.