writing / kubernetes

Stop Using Cluster-Admin for Everything

If your service account can delete every pod in the cluster, you’re one compromised workload away from a cluster-wide incident.

Atharva Uday UndeAtharva Uday UndeJuly 14, 20267 min read
kubernetesrbacsecuritydevops

The Default Path of Least Resistance

You’re setting up a new service in EKS. Maybe it’s a backup job, a custom controller, or an observability agent. You create a ClusterRole, bind it to your service account, and realize you aren't sure exactly which resources it needs to watch.

You type this:

rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]

It works. Your logs stop showing 403 Forbidden errors. You move on to the next task.

This is the Kubernetes equivalent of chmod 777 on your production database. When you grant cluster-admin (or even a broad list and patch over all resources), you aren’t just giving your application permission to do its job. You are giving any compromised dependency in that container the keys to the entire kingdom.


Why You Need Namespace Isolation

Kubernetes RBAC is powerful because it allows for granular control. If your application only needs to restart pods in namespace-a, it has no business knowing that namespace-b exists.

When you use ClusterRole and ClusterRoleBinding, you are defining permissions globally. If that service account is compromised, the blast radius is the entire cluster.

Switch to Role and RoleBinding. Here is what a properly scoped pair looks like:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-reader
  namespace: production-app
rules:
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-reader-binding
  namespace: production-app
subjects:
  - kind: ServiceAccount
    name: my-app-sa
    namespace: production-app
roleRef:
  kind: Role
  name: app-reader
  apiGroup: rbac.authorization.k8s.io

A Role only applies to a single namespace. If you restrict your service account to a specific namespace, an attacker who gains execution in your pod can't scrape secrets from other namespaces, can't delete your ingress controller, and can't mess with your cert-manager.

Two caveats before you treat namespaces as a security boundary:

  1. RBAC only gates the API server. A pod with namespace-scoped RBAC can still talk to any other pod over the network. If you want actual isolation, pair your Role with a NetworkPolicy.
  2. Some resources are cluster-scoped. Nodes, PersistentVolumes, and CRD definitions can't be granted through a Role. If your workload genuinely needs one of these, use a narrowly scoped ClusterRole with a ClusterRoleBinding — there's no way to namespace-scope access to a cluster-scoped resource. (The ClusterRole + RoleBinding trick only works the other way around: it lets you define namespaced permissions once and reuse them per namespace.)

The "I Don't Know What I Need" Workflow

If you’re struggling to scope your RBAC, stop guessing. Use kubectl auth can-i to test, and use the kubectl-who-can plugin to audit.

Here is the process I use when setting up a new IAM-to-K8s role mapping:

# 1. Create a minimal Role
kubectl create role app-reader \
  --verb=get,list,watch \
  --resource=pods,services \
  --namespace=production-app

# 2. Create the binding
kubectl create rolebinding app-reader-binding \
  --role=app-reader \
  --serviceaccount=production-app:my-app-sa \
  --namespace=production-app

# 3. Test the permission as the service account
kubectl auth can-i get pods \
  --as=system:serviceaccount:production-app:my-app-sa \
  --namespace=production-app

If kubectl auth can-i returns yes, you’re done. If it returns no, you add exactly the verb or resource the application actually failed on — the 403 Forbidden in your logs tells you which one — and re-test. You never touch * again.

Two things this workflow won't catch:

  • kubectl auth can-i only evaluates RBAC. Admission controllers (OPA/Gatekeeper, Kyverno, Pod Security Admission) can still deny a request that RBAC allows, so a yes here isn't a guarantee the operation succeeds.
  • Guessing verb-by-verb doesn't scale for complex workloads like controllers. For those, run the workload in a dev cluster with API server audit logging enabled and feed the logs to audit2rbac, which generates a Role covering exactly the calls the workload actually made.

The Audit: Who Can Do What?

If you want to be truly terrified, look at how many things in your cluster have secret access.

Run this — and note it checks ClusterRoleBindings too, because that's where the truly dangerous grants live:

kubectl get rolebindings,clusterrolebindings --all-namespaces -o json \
  | jq -r '.items[]
    | select(.roleRef.name == "admin" or .roleRef.name == "cluster-admin")
    | "\(.kind)/\(.metadata.name): \([.subjects[]? | "\(.kind)/\(.name)"] | join(", "))"'

If you see service accounts from your application namespaces popping up here, investigate why they need those permissions. Some tools legitimately do — GitOps controllers like Argo CD and Flux, backup tools like Velero, the cluster autoscaler. But cluster-wide admin access should be rare, deliberate, and documented. An unexplained entry in this list is a privilege escalation path waiting to be used.

While you're at it, grep your roles for the verbs escalate, bind, and impersonate. Any of these lets a subject grant itself permissions it doesn't currently have — they're cluster-admin with extra steps.

One more thing to check: RBAC aggregation labels. A ClusterRole carrying a label like this silently extends Kubernetes' built-in roles:

metadata:
  labels:
    rbac.authorization.k8s.io/aggregate-to-admin: "true"

Every rule in that role gets merged into the built-in admin role cluster-wide (the same applies to aggregate-to-edit and aggregate-to-view). Operators use this to make their CRDs manageable through the standard roles, which is convenient — but it means an innocent-looking helm install can quietly widen what "admin" means in every namespace. Grep for aggregate-to- when reviewing third-party manifests.

Why this matters for the "Boring Infrastructure" mindset

Novelty-seeking engineers love installing third-party operators that demand cluster-admin access. "Oh, it needs to monitor everything," they say.

Does it?

Most operators don't need to touch your secrets. Many don't need to list your nodes. Before you apply that install.yaml from a GitHub repo, open it. Look for rules. If you see a wildcard, edit the manifest locally before applying it — or check whether the project documents a reduced-permission or namespace-scoped installation profile; many mature projects publish one.

If the vendor says "it won't work without cluster-wide permissions," ask yourself if you really need that vendor. An observability tool that requires permission to read every secret in your cluster is an observability tool that is a massive security liability.


Practical Checklist for RBAC Hygiene

  1. Deny by default: Many engineers assume Kubernetes defaults to allow. It doesn't — RBAC is strictly deny-by-default, and every permission a workload has is one someone granted. Keep it that way: start with an empty Role and add permissions only after you see a 403 in the logs.
  2. Use Roles, not ClusterRoles: If a service only operates in one namespace, it should never have a ClusterRoleBinding. When a ClusterRole is unavoidable and its rules only cover namespaced resources, bind it with a RoleBinding to limit where those permissions apply — remember, cluster-scoped resources can't be namespace-scoped no matter how you bind them.
  3. Limit verbs: Read-only workloads need only get, list, and watch. Grant patch, update, or delete only when the workload genuinely modifies resources — controllers and GitOps tools legitimately need them; your average app does not. Treat escalate, bind, and impersonate as radioactive.
  4. Scope secret access by name: If a workload needs one secret, use resourceNames to grant get on exactly that secret — not get on all secrets in the namespace. (One catch: resourceNames doesn't work with list or watch, so those verbs still expose everything — which is also why controllers, which rely on watch, usually can't use it.)
  5. Automate the audit: Add a CI check that scans your manifests for wildcards in RBAC rules — kube-linter and Kubescape both do this out of the box. If it finds one, break the build.

Security is about making it hard for an attacker to move laterally. If you lock down your RBAC today, you make the attacker's life significantly harder tomorrow. That’s not "overhead"—that’s insurance.

Tags: kubernetes · rbac · security · devops