Back to articles

Why your RBAC bindings are probably too permissive — and how to audit them in an hour

RBAC sprawl accumulates invisibly. This article provides a methodology for identifying problematic bindings in about an hour — and a framework for tightening them without causing an incident.

Kubernetes RBAC sprawl is one of those problems that accumulates invisibly. Nobody wakes up and decides to grant cluster-admin to a service account. Instead, someone hits a permissions error during a late-night deployment, adds a binding "temporarily," and moves on. Six months later, you have dozens of bindings that nobody remembers creating, half of which grant permissions that nobody needs. This article is a methodology for identifying those bindings in about an hour — and a framework for tightening them without causing an incident.

Who this is for: Platform engineers, security teams, and anyone responsible for cluster access control. You should be comfortable with kubectl and have cluster-admin access to the clusters you are auditing.

What you will walk away with: A repeatable one-hour audit process, a list of red flags to prioritize, and concrete remediation strategies.


The mental model

RBAC answers one question: who can do what to which resources?

The system has three components:

Sprawl happens when bindings accumulate faster than they are reviewed. Each binding seems reasonable in isolation. But over time, the total set of permissions granted across the cluster becomes impossible to reason about — and invariably includes permissions that nobody needs and nobody remembers granting.


Why this matters: a real-world scenario

Here is how RBAC sprawl becomes a security incident:

A CI/CD pipeline needs to deploy to multiple namespaces. An engineer grants its service account cluster-admin because scoping permissions properly takes time they do not have. The pipeline works. Months pass.

An attacker compromises a developer laptop and extracts the CI system's kubeconfig. Or they find the service account token in a misconfigured log aggregator. Or they exploit an RCE in the CI runner itself.

With cluster-admin, they can:

The blast radius of a single overly permissive binding is the entire cluster. This is why RBAC hygiene matters.


How RBAC sprawl happens

RBAC sprawl follows predictable patterns. Understanding them helps you know where to look.

The "make it work" binding. A developer hits a permissions error. They search the internet, find a Stack Overflow answer suggesting cluster-admin, and apply it. The deployment works. The binding stays.

The inherited binding. A new team copies the RBAC configuration from an existing team without understanding what it does. Both teams now have the same overly permissive bindings.

The operator binding. You install a Helm chart or operator that creates its own service account and ClusterRoleBinding. The operator works, so you never inspect what permissions it actually has. Some operators request cluster-admin when they only need access to their own CRDs.

The default service account problem. Every namespace has a default service account. If someone binds a role to system:serviceaccount:*:default — or worse, creates a ClusterRoleBinding — every pod that does not explicitly specify a service account now has those permissions.

The abandoned binding. A service account or user is deleted, but the binding that references it remains. These orphaned bindings clutter your audit output and can mask real problems.


The one-hour audit methodology

This audit is structured as four passes. Each pass takes about 15 minutes and produces a list of bindings that need attention. You will need kubectl with cluster-admin access and, optionally, a few community tools.

Pass 1: Inventory all bindings (15 minutes)

Start by getting visibility into what exists.

List all ClusterRoleBindings:

kubectl get clusterrolebindings -o wide

This gives you a count and a sense of the scope. A typical production cluster might have 30-80 ClusterRoleBindings. If you have 200+, you have sprawl.

Export ClusterRoleBindings for analysis:

kubectl get clusterrolebindings -o json > clusterrolebindings.json

List all RoleBindings across all namespaces:

kubectl get rolebindings -A -o wide

Export RoleBindings for analysis:

kubectl get rolebindings -A -o json > rolebindings.json

Summarize binding counts per namespace:

kubectl get rolebindings -A --no-headers | awk '{print $1}' | sort | uniq -c | sort -rn

Namespaces with high binding counts deserve closer inspection. If kube-system has 50 RoleBindings, that is probably fine — it is the control plane namespace. If your application namespace has 50 RoleBindings, something is wrong.

Pass 2: Identify cluster-admin and wildcard bindings (15 minutes)

These are the highest-risk bindings. Start here.

Find all bindings to cluster-admin:

kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name + " -> " + (.subjects // [] | map(.name) | join(", "))'

Every result from this command is a binding that grants full cluster access. Each one should have a documented justification. In a well-maintained cluster, this list should be short — perhaps 5-10 entries at most.

Find wildcards in ClusterRoles:

Wildcards (*) in verbs, resources, API groups, or nonResourceURLs mean "everything." They are rarely necessary.

kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]? | (.verbs // [] | contains(["*"])) or (.resources // [] | contains(["*"])) or (.apiGroups // [] | contains(["*"])) or (.nonResourceURLs // [] | contains(["*"]))) | .metadata.name'

This produces a list of ClusterRoles that use wildcards. Now find what binds to them:

for role in $(kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]? | (.verbs // [] | contains(["*"])) or (.resources // [] | contains(["*"])) or (.nonResourceURLs // [] | contains(["*"]))) | .metadata.name'); do
  echo "=== $role ==="
  kubectl get clusterrolebindings -o json | jq -r --arg role "$role" '.items[] | select(.roleRef.name == $role) | .metadata.name + " -> " + (.subjects // [] | map(.kind + "/" + .name) | join(", "))'
done

Note: On large enterprise clusters, this loop makes repeated API calls which can cause throttling. For better performance, use the JSON files exported in Pass 1 to do the cross-reference entirely client-side:

# Export ClusterRoles if not already done
kubectl get clusterroles -o json > clusterroles.json

# Cross-reference locally without additional API calls
jq -r '.items[] | select(.rules[]? | (.verbs // [] | contains(["*"])) or (.resources // [] | contains(["*"])) or (.nonResourceURLs // [] | contains(["*"]))) | .metadata.name' clusterroles.json | while read role; do
  echo "=== $role ==="
  jq -r --arg role "$role" '.items[] | select(.roleRef.name == $role) | .metadata.name + " -> " + (.subjects // [] | map(.kind + "/" + .name) | join(", "))' clusterrolebindings.json
done

Find system:masters group usage:

Members of the system:masters group effectively have cluster-admin access via a built-in binding. Check who is in it:

kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.subjects[]? | select(.kind == "Group" and .name == "system:masters")) | .metadata.name'

If anything other than the built-in cluster-admin binding appears, investigate immediately.

Find aggregated ClusterRoles:

ClusterRoles with aggregationRule inherit permissions dynamically from other roles. These can gain new permissions when additional roles with matching labels are created — making them harder to reason about over time and easy to miss in audits:

kubectl get clusterroles -o json | jq -r '.items[] | select(.aggregationRule) | .metadata.name'

For each aggregated role, check what labels it matches and which roles contribute permissions:

kubectl get clusterrole <role-name> -o yaml

Pass 3: Find bindings to default service accounts (15 minutes)

Default service accounts are a common source of privilege escalation.

Find ClusterRoleBindings to any default service account:

kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.subjects[]? | select(.kind == "ServiceAccount" and .name == "default")) | .metadata.name + " in " + (.subjects[] | select(.kind == "ServiceAccount" and .name == "default") | .namespace // "cluster-wide")'

Find RoleBindings to the default service account in each namespace:

kubectl get rolebindings -A -o json | jq -r '.items[] | select(.subjects[]? | select(.kind == "ServiceAccount" and .name == "default")) | .metadata.namespace + "/" + .metadata.name'

Each result is a binding that grants permissions to every pod in that namespace that does not explicitly set a service account. This is almost never intentional.

Check if pods are using default service accounts:

kubectl get pods -A -o json | jq -r '.items[] | select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null) | .metadata.namespace + "/" + .metadata.name'

If this list is long, pods are running with whatever permissions are bound to the default service account.

Pass 4: Find orphaned bindings (15 minutes)

Bindings to subjects that no longer exist clutter your audit and can mask real issues.

Find bindings to non-existent service accounts:

for binding in $(kubectl get clusterrolebindings -o jsonpath='{.items[*].metadata.name}'); do
  subjects=$(kubectl get clusterrolebinding "$binding" -o json | jq -r '.subjects[]? | select(.kind == "ServiceAccount") | .namespace + "/" + .name')
  for subject in $subjects; do
    ns=$(echo "$subject" | cut -d'/' -f1)
    sa=$(echo "$subject" | cut -d'/' -f2)
    if ! kubectl get serviceaccount "$sa" -n "$ns" &>/dev/null; then
      echo "Orphaned: $binding -> $subject"
    fi
  done
done

This script is slow but thorough. Run it once during your audit.

Find bindings to non-existent users or groups:

This is harder to automate because Kubernetes does not maintain a user directory — users are defined by your authentication system. However, you can list all user subjects and review them manually:

kubectl get clusterrolebindings -o json | jq -r '.items[] | .subjects[]? | select(.kind == "User") | .name' | sort -u

Cross-reference this list with your identity provider. In OIDC-based setups, groups may be managed externally and not visible in Kubernetes, so treat this list as advisory rather than definitive.


Red flags to prioritize

Not all findings are equally urgent. Here is a ranking:

Critical — fix immediately:

High — fix this week:

Medium — fix this sprint:

The escalate, bind, and impersonate verbs deserve special attention. These are escalation primitives:

Find subjects with escalation permissions:

kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]? | .verbs // [] | (contains(["escalate"]) or contains(["bind"]) or contains(["impersonate"]))) | .metadata.name'

Tools that help

Manual kubectl commands work but are tedious for regular audits. Several tools make this faster:

kubectl auth can-i (built-in)

The simplest way to test what a subject can do. No installation required:

# Check if a service account can perform an action
kubectl auth can-i get secrets --as=system:serviceaccount:my-ns:my-sa

# List all permissions for a service account
kubectl auth can-i --list --as=system:serviceaccount:my-ns:my-sa

# Check in a specific namespace
kubectl auth can-i create deployments -n production --as=system:serviceaccount:ci:deployer

rbac-lookup (by FairwindsOps)

Lists all roles and bindings for a given subject. Useful for answering "what can this service account do?"

# Install
kubectl krew install rbac-lookup

# Usage
rbac-lookup my-service-account -k serviceaccount -n my-namespace

kubectl-who-can (by AquaSecurity)

Answers "who can perform this action?" — the inverse of rbac-lookup.

# Install
kubectl krew install who-can

# Usage
kubectl who-can get secrets -A
kubectl who-can create clusterrolebindings

rakkess (by corneliusweig)

Shows a matrix of what resources a subject can access.

# Install
kubectl krew install access-matrix

# Usage
kubectl access-matrix --sa my-namespace:my-service-account

KubiScan (by CyberArk)

A comprehensive RBAC scanner that identifies risky permissions automatically.

# Install and run
git clone https://github.com/cyberark/KubiScan.git
cd KubiScan
pip install -r requirements.txt
python KubiScan.py --risky-roles

Remediation strategies

Finding problems is the easier part. Fixing them without breaking production is harder. Here is how to approach remediation:

Tightening without breaking things

Step 1: Identify what the workload actually does

Before removing permissions, understand what the workload needs. Check the workload's code or documentation. If you do not know what it does, you do not know what permissions it needs.

If your cluster has audit logging enabled, query the audit logs to see what API calls the service account actually makes. This is the safest way to scope permissions — you are working from observed behavior rather than guessing:

# Example: filter audit logs for a specific service account
# (exact query depends on your logging backend: Cloud Logging, CloudWatch, etc.)
# This example assumes local JSON logs:
cat audit.log | jq 'select(.user.username == "system:serviceaccount:my-ns:my-sa")'

Look at the verb, resource, and namespace fields to understand what the workload actually accesses. If the workload only ever calls get on ConfigMaps in its own namespace, it does not need cluster-wide access to Secrets.

Step 2: Create a scoped replacement role

Do not modify existing ClusterRoles if they are used by multiple bindings. Create a new Role (namespace-scoped) or ClusterRole with only the required permissions.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: my-app-role
  namespace: my-namespace
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch"]
  resourceNames: ["my-app"]  # Scope to specific resources when possible

Step 3: Create the new binding

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-app-binding
  namespace: my-namespace
subjects:
- kind: ServiceAccount
  name: my-app-sa
  namespace: my-namespace
roleRef:
  kind: Role
  name: my-app-role
  apiGroup: rbac.authorization.k8s.io

Step 4: Test in a non-production environment

Apply the new binding alongside the old one. Run the workload. Check logs for permission errors. If there are none, remove the old binding.

Step 5: Remove the old binding

kubectl delete clusterrolebinding my-overly-permissive-binding

Before and after: a real example

Before (discovered during audit):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ci-deployer-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: ci-deployer
  namespace: ci-system

This grants full cluster-admin to the CI/CD service account. It was created because someone needed to deploy to multiple namespaces and "cluster-admin worked."

After (remediated):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ci-deployer
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "daemonsets", "statefulsets"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["services", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ci-deployer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: ci-deployer
subjects:
- kind: ServiceAccount
  name: ci-deployer
  namespace: ci-system

The CI system can still deploy workloads to any namespace but cannot read secrets, delete namespaces, modify RBAC, or access the control plane.


Ongoing RBAC hygiene

A single audit fixes the current state but does not prevent future sprawl. Build ongoing practices:

Review RBAC changes in code review. If RBAC manifests live in Git (they should), require explicit review for any binding changes. Treat ClusterRoleBindings like you treat production database access.

Audit quarterly. Run this methodology every quarter. Track the number of cluster-admin bindings and wildcard roles over time. The numbers should be stable or decreasing.

Use admission control. Tools like OPA Gatekeeper or Kyverno can prevent overly permissive bindings from being created in the first place:

# Example Kyverno policy: block bindings to cluster-admin outside kube-system
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-cluster-admin
spec:
  validationFailureAction: Enforce
  rules:
  - name: block-cluster-admin-bindings
    match:
      any:
      - resources:
          kinds:
          - ClusterRoleBinding
    preconditions:
      all:
      - key: "{{ request.object.roleRef.name }}"
        operator: Equals
        value: cluster-admin
    validate:
      message: "ClusterRoleBindings to cluster-admin require security team approval"
      deny: {}

Separate service accounts per workload. Do not share service accounts across deployments. Each workload should have its own service account with its own scoped permissions. This makes auditing tractable and limits blast radius.

Disable service account token automounting by default. Most pods do not need to talk to the Kubernetes API. Even with short-lived, automatically rotated projected tokens, disabling automount reduces unnecessary exposure. Set automountServiceAccountToken: false at the namespace level or in each service account:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-sa
  namespace: my-namespace
automountServiceAccountToken: false

Tradeoffs

This methodology has costs.

Time. The initial audit takes about an hour. Remediation takes longer — potentially days or weeks depending on how many problematic bindings you find and how well you understand your workloads.

Risk of breaking things. Tightening permissions can break workloads if you scope too narrowly. The mitigation is testing in non-production first and keeping the old bindings in place during the transition period.

Ongoing maintenance. RBAC hygiene is not a one-time effort. If you are not reviewing RBAC changes in code review and auditing quarterly, sprawl will return.

Operator complexity. Some operators genuinely need broad permissions. Distinguishing "this operator needs cluster-wide access" from "this operator was lazy about scoping" requires understanding what the operator does. Sometimes the answer is "use a different operator."


Summary

RBAC sprawl is a universal problem in long-running Kubernetes clusters. The one-hour audit in this article gives you a structured way to find the worst offenders:

  1. Inventory — Get visibility into all bindings
  2. Wildcards and cluster-admin — Find the highest-risk permissions
  3. Default service accounts — Find unintended privilege inheritance
  4. Orphaned bindings — Clean up references to deleted subjects

Remediation is iterative: create scoped replacements, test, then remove the old bindings. And build ongoing practices — code review, quarterly audits, admission control — so the sprawl does not return.

The goal is not zero bindings. The goal is that every binding exists for a documented reason and grants only the permissions the workload actually needs. That is what least privilege means in practice.