The access problem nobody notices until an audit
Kubernetes ships with a capable authorisation system and, in most clusters, an access model nobody designed. Early on, an engineer needed to get unblocked, so they were given cluster-admin. A CI job needed to deploy, so it got a broad ClusterRole. Months later, a contractor leaves, and nobody can say which of forty bindings they held.
Kubernetes RBAC with least privilege is the discipline of replacing that history with a model you can explain. It is a recurring request in our Kubernetes consulting engagements, and a standard section of every production readiness audit.
How RBAC works, briefly
RBAC has four objects. A Role grants permissions inside one namespace, a ClusterRole grants them cluster-wide or can be reused in a namespace, and a RoleBinding or ClusterRoleBinding attaches a role to a user, group or service account. The key rule: a ClusterRole bound through a RoleBinding is limited to that namespace, which lets you define a role once and reuse it safely per namespace.
RBAC is additive only. There are no deny rules, so the way to restrict access is to grant less.
What good looks like
- Humans come from your identity provider, and bindings reference groups, never individuals.
- A handful of well named roles per job function, not a role per person.
- Namespace-scoped access by default. Cluster-wide rights belong to a small platform group.
- No standing cluster-admin for daily work. Elevated access is time-limited and logged.
- Automation uses dedicated service accounts with only the verbs it needs.
- Every binding has an owner, and a review process removes stale ones.
The role model we start from
We tailor this with your team leads, but the shape is usually:
| Role | Scope | Can do |
|---|---|---|
| viewer | Namespace | Read workloads, events, logs; no secrets |
| developer | Team namespaces | Manage deployments, services, config in non-production; read in production |
| operator | Cluster | Restart workloads, drain nodes, read most resources |
| platform-admin | Cluster | Manage CRDs, RBAC, cluster add-ons; use sparingly |
A namespace scoped example, reusing a built-in role:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: payments-developers-edit
namespace: payments
subjects:
- kind: Group
name: oidc:payments-developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
You can test the effect before and after any change:
kubectl auth can-i --list --namespace payments --as-group oidc:payments-developers --as dummy
kubectl auth can-i get secrets -n payments --as dummy --as-group oidc:payments-developers
Note that the built-in edit role can read secrets in its namespace, which may be more than you want for some teams. When it is, we write a custom Role instead.
Connecting to your cloud identity
On EKS, IAM principals map to Kubernetes users and groups through access entries (or the older aws-auth ConfigMap). On AKS, Entra ID groups feed straight into bindings. On GKE, Google identities and groups work with RBAC. In each case the principle is the same: the identity provider owns who someone is and which groups they belong to, and Kubernetes owns what those groups may do. Joiners and leavers then need only a directory change.
Service accounts and workload identity
Most real-world incidents involve a workload credential, not a human one. We tighten three things:
- Default service account. Pods that do not call the API should not mount a token. Set
automountServiceAccountToken: falseon service accounts and pod specs where it is not needed. - Per-workload service accounts with narrowly scoped Roles instead of sharing one account across an app.
- Cloud permissions via workload identity (IRSA or EKS Pod Identity, Entra Workload ID, GKE Workload Identity), not through the node’s role, which every pod on the node would inherit.
Escalation paths to remove
Some permissions are more powerful than they look:
get,listorwatchon secrets.createon pods, orpods/exec, which can reach other service accounts’ tokens in the namespace.impersonate,escalateandbindon roles.- Wildcards (
*) in verbs, resources or apiGroups. nodes/proxyand access to the kubelet API.
We scan for these, then decide case by case. Blocking all of them breaks real work, so the answer is usually to restrict them to a few well controlled identities.
Worked example: removing a standing cluster-admin grant
Say five engineers hold cluster-admin through a single binding to a group. We do not delete it on day one. First we create the replacement roles and bind them to the same group, then ask the engineers to work with the narrower access for a week and report anything blocked. Each blocked action is either a legitimate need, which we add to a role, or a habit, which we retire.
Only then do we remove the old binding, keeping a break-glass role that requires an approval step and expires on its own. Because the sequence is additive first and subtractive last, nobody loses access mid-incident.
Trade-offs and when RBAC alone is not enough
RBAC controls API access. It does not stop a compromised pod from talking to other pods (that is network policy), or from running as root (that is pod security standards). For policy that RBAC cannot express, such as forbidding privileged containers, we add admission control with Kyverno or OPA Gatekeeper. For very small teams, an elaborate role model can cost more than it protects. Four roles and a good identity group structure is plenty.
Common failure modes
- Fine-grained roles that nobody understands, so everyone asks for cluster-admin.
- A CI service account with cluster-admin because narrowing it took effort.
- Dev and production sharing identities, so a developer’s laptop can change production.
- Role changes made by hand, never recorded. We store RBAC in Git and apply it through GitOps so changes are reviewable.
- Reviews done once and never repeated.
Delivery and how to tell it works
We deliver the role model, bindings as code, the identity mapping, a list of removed grants, and a review checklist. Signs of success: cluster-admin is bound to a short, named list; a new joiner gets access by joining a group; and an access review takes an hour, not a week. For wider cloud context see our Terraform work on IAM. To discuss your own cluster, get in touch.