Kubernetes RBAC Security Best Practices for Production Clusters
Production Kubernetes RBAC: namespace Roles not cluster-admin, automount off, bound tokens, aws-auth vs access entries, and kubectl-who-can in CI.
This is Kubernetes API authorization: Roles, RoleBindings, service account tokens, and the cloud IAM mapping that can bypass all of that. It is not NetworkPolicy, not Pod Security, and not a CNAPP overview. Those belong in cloud-native application security. API behavior: Using RBAC.
kubectl / CI
→ API server (RBAC on this request)
→ pod SA token (if automounted)
→ cloud IAM via IRSA / Workload Identity / Entra (second control plane)
A tight Role is worthless if the same human is system:masters through aws-auth.
1. No ClusterRoleBinding for application service accounts
# Wrong: deploy SA is cluster-admin
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: payments-deploy
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: deploy
namespace: payments
Replace with a Role in payments:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: payments-app
namespace: payments
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["payments-api"]
get on a named Secret is not list on secrets in the namespace. list dumps every secret to anyone who can compromise the SA.
Aggregated ClusterRoles (rbac.authorization.k8s.io/aggregate-to-admin: "true") hide verbs. Read the aggregated rules, not the ClusterRole name.
2. Default SA: automount off
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: payments
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-api
namespace: payments
# automount only if this process talks to the API
On the Pod spec, automountServiceAccountToken: false unless you need it. Bound tokens (TokenRequest) with expirationSeconds and audiences replace the long-lived Secret-based token.
Failure mode: Istio/Linkerd sidecars and operators do need a token. That is a dedicated SA, not “turn automount back on for default.”
3. Prove it with who-can, in CI
kubectl-who-can get secrets -n payments
kubectl-who-can get secrets --all-namespaces
kubectl auth can-i --list --as=system:serviceaccount:payments:payments-api -n payments
Pipeline on rendered Helm:
- Fail if an application chart contains
kind: ClusterRoleBindingtocluster-admin. - Fail if
resources: ["secrets"]withverbscontaininglistor*at cluster scope. - Optional: apply to an ephemeral kind/k3d cluster and run who-can.
Emergency Roles added at 2 a.m. never expire. Diff ClusterRoleBinding objects weekly against git.
4. Cloud mappings that skip Kubernetes RBAC
| Cluster | Mapping | Footgun |
|---|---|---|
| EKS | Access Entries (preferred) or aws-auth ConfigMap |
system:masters for an IAM role used by CI |
| GKE | Google Groups + GKE RBAC, Workload Identity | Default Compute SA still Editor on GCP |
| AKS | Entra ID / AKS-managed Entra | Entire AAD-Cluster-Admins group → cluster-admin |
EKS Access Entries:
aws eks list-access-entries --cluster-name prod
aws eks describe-access-entry --cluster-name prod --principal-arn "$ARN"
If you still use aws-auth, a merge conflict during an add-on upgrade can re-add mapRoles to system:masters. Treat that ConfigMap as production IAM.
Workload identity (IRSA / GKE WI / AKS workload identity) is a second RBAC system: the pod SA may be namespace-scoped in Kubernetes and s3:* in AWS. Restrict both. Graph correlation of pod SA → cloud role is attack path analysis, after these bindings are not *.
5. RBAC is not PSS and not NetworkPolicy
| Layer | Stops |
|---|---|
| RBAC | API verbs (get secret, create pod) |
| Pod Security Admission | Privileged, hostNetwork, hostPath |
| Admission (Kyverno/Gatekeeper) | Unsigned images, missing labels |
| NetworkPolicy | Pod IP traffic |
RBAC will not stop a privileged pod if the deployer already had create pods. Layer them; do not write a “Kubernetes security” mega-post here.
Checklist
- Default SA:
automountServiceAccountToken: falsein every namespace - App SAs: namespace Role, named Secret
get, no cluster-admin - CI: fail ClusterRoleBinding to cluster-admin in app charts
- who-can on
secretscluster-wide is only platform SAs - EKS Access Entries / aws-auth / Entra groups reviewed after every cluster bump
- IRSA/WI roles are not
AdministratorAccess
Key takeaways
- cluster-admin on a deploy SA is a cluster credential in every pod that mounts it.
- automount false on default is the smallest high-leverage change.
- who-can in CI catches chart drift; live clusters drift after hotfixes.
- Cloud IAM mappings can grant
system:masterswithout a single RoleBinding.
Related: Cloud-native application security · GCP IAM hardening