Skip to main content

Ensure The Cluster-Admin Role Is Only Used Where Required

More Info:

The cluster-admin ClusterRole grants unrestricted access to the entire cluster. It should only be bound to principals that strictly require full administrative control.

Risk Level

Critical

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. List all cluster-admin bindings and their subjects

    • Run on: any machine with kubectl access
    • Command:
      kubectl get clusterrolebindings -o wide | grep 'cluster-admin'
      kubectl get clusterrolebindings -o yaml | grep -A10 -B2 'name: cluster-admin'
    • Purpose: identify every ClusterRoleBinding that grants cluster-admin and who/what it is bound to (users, groups, service accounts).
  2. For each binding, understand what is using it and why

    • For each binding name found (e.g. cluster-admin, aks-cluster-admin-binding, etc.), inspect details:
      kubectl get clusterrolebinding <binding-name> -o yaml
    • Map each subject to:
      • Human user / group (e.g. corporate IdP group)
      • Service account (note its namespace and owning deployment/workload)
      • System or add-on component (e.g. CNI, Ingress, monitoring)
    • Check your internal documentation, IaC (Helm, Terraform, etc.), or platform docs to see why this binding was created.
  3. Decide if full cluster-admin is strictly necessary for each subject
    For each subject in each binding, ask:

    • Does it require cluster-wide, unrestricted access (e.g. break-glass admin, platform SRE)?
    • Is its actual function limited (e.g. namespaced app, read-only monitor, single-namespace operator)?
    • Is there a documented support / vendor requirement for cluster-admin, or can it be scoped down (namespace-scoped or specific API groups/resources/verbs)?
    • If multiple subjects are in one binding, evaluate each individually; you may need to split them into separate bindings with different roles.
  4. Design and apply least-privilege alternatives where possible

    • If cluster-admin is not strictly required, create or reuse a less-privileged ClusterRole/Role that matches the real needs. Examples (adapt to your case):
      # Example: read-only cluster-wide role
      kubectl apply -f - <<'EOF'
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRole
      metadata:
      name: readonly-cluster
      rules:
      - apiGroups: [""]
      resources: ["pods","services","configmaps","namespaces"]
      verbs: ["get","list","watch"]
      EOF
    • Bind that role to the subject instead of cluster-admin (adjust kind, name, namespace as identified in step 2):
      kubectl apply -f - <<'EOF'
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
      name: readonly-cluster-binding
      subjects:
      - kind: User
      name: <user-identifier>
      roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: readonly-cluster
      EOF
    • For service accounts that only need namespace access, prefer Role + RoleBinding in that namespace.
  5. Remove or narrow the cluster-admin bindings

    • After confirming the replacement access works (functional tests, user confirmation, or workload logs), remove or adjust the cluster-admin binding:
      • To completely remove a no-longer-needed binding:
        kubectl delete clusterrolebinding <binding-name>
      • To keep the binding but drop unnecessary subjects, edit it interactively:
        kubectl edit clusterrolebinding <binding-name>
        Remove only the subjects that no longer require cluster-admin, then save.
    • For bindings managed by GitOps/IaC, make the equivalent changes in the source manifests instead of editing live objects directly, then apply/sync.
  6. Verify remaining usage of cluster-admin is minimal and intentional

    • Run on: any machine with kubectl access
    • Command:
      kubectl get clusterrolebindings --no-headers | grep 'cluster-admin' || echo "No cluster-admin bindings found"
    • For any remaining bindings, ensure you have documented justification (who, why, and approval) and that they are periodically re-reviewed.
Using kubectl

Using kubectl

Run these commands from any machine with kubectl access.

  1. List all ClusterRoleBindings that grant cluster-admin:
kubectl get clusterrolebindings -o wide | grep 'cluster-admin'

If you get no output, there are no current bindings to cluster-admin (no problem for this check).
Any lines returned indicate a binding that must be reviewed.

  1. View full details of each binding found:
# Example: replace <binding-name> with each name from step 1
kubectl get clusterrolebinding <binding-name> -o yaml

Focus on:

  • .roleRef.name – must be cluster-admin for this control.
  • .subjects – which User, Group, or ServiceAccount is getting cluster-admin.

Problem indicators:

  • Bindings to broad identities such as:
    • system:authenticated, system:unauthenticated, or other large groups
    • Wildcard-like groups used by your IdP (e.g., “AllEmployees”)
  • ServiceAccounts in application namespaces (e.g., default, dev-*, prod-*) that do not clearly require full cluster control.
  • External users or groups where a more specific, least-privilege ClusterRole could be used instead.
  1. (Optional) Quickly list just the subjects of all cluster-admin bindings:
kubectl get clusterrolebindings -o json \
| jq -r '.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="cluster-admin")
| .metadata.name as $b
| .subjects[]?
| [$b, .kind, .name, (.namespace // "-")]
| @tsv'

Output columns:

  1. ClusterRoleBinding name
  2. Subject kind (User, Group, ServiceAccount)
  3. Subject name
  4. Namespace (- for non-namespaced subjects)

Problem indicators here:

  • Many different subjects listed, especially generic groups.
  • Any entry where you cannot clearly justify why that subject needs unrestricted cluster-wide admin access.
Automation
#!/usr/bin/env bash
# Report all uses of the cluster-admin ClusterRole and highlight likely risks.
# Run on: any machine with kubectl access and current-context set to target cluster.

set -euo pipefail

echo "== ClusterRoleBindings that grant cluster-admin =="
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "cluster-admin")
| [
.metadata.name,
.subjects // [] | map(.kind + "/" + .name + (if .namespace then " (ns:" + .namespace + ")" else "" end)) | join(", ")
]
| @tsv
' \
| awk 'BEGIN { printf "%-40s %-60s\n", "CLUSTERROLEBINDING", "SUBJECTS"; print gensub(/./,"-","g",sprintf("%-40s %-60s"," "," ")); } { printf "%-40s %-60s\n", $1, substr($0, index($0,$2)) }'

echo
echo "== ClusterRoleBindings with cluster-admin bound to ServiceAccounts (detail) =="
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "cluster-admin")
| {
name: .metadata.name,
subjects: (.subjects // [])
}
| . as $b
| $b.subjects[]
| select(.kind == "ServiceAccount")
| [
$b.name,
.name,
(.namespace // "<none>")
]
| @tsv
' \
| awk 'BEGIN { printf "%-40s %-40s %-20s\n", "CLUSTERROLEBINDING", "SERVICEACCOUNT", "NAMESPACE"; print gensub(/./,"-","g",sprintf("%-40s %-40s %-20s"," "," "," ")); } { printf "%-40s %-40s %-20s\n", $1, $2, $3 }'

echo
echo "== ClusterRoleBindings with cluster-admin bound to Users/Groups (detail) =="
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "cluster-admin")
| {
name: .metadata.name,
subjects: (.subjects // [])
}
| . as $b
| $b.subjects[]
| select(.kind == "User" or .kind == "Group")
| [
$b.name,
.kind,
.name
]
| @tsv
' \
| awk 'BEGIN { printf "%-40s %-10s %-60s\n", "CLUSTERROLEBINDING", "KIND", "IDENTITY"; print gensub(/./,"-","g",sprintf("%-40s %-10s %-60s"," "," "," ")); } { printf "%-40s %-10s %-60s\n", $1, $2, $3 }'

echo
echo "== Summary counts =="
kubectl get clusterrolebindings -o json \
| jq '
.items
| map(select(.roleRef.kind=="ClusterRole" and .roleRef.name=="cluster-admin")) as $b
| {
total_clusterrolebindings_using_cluster_admin: ($b | length),
total_subjects: ($b | map(.subjects // []) | add | length),
by_kind: ($b | map(.subjects // []) | add | group_by(.kind) | map({kind: .[0].kind, count: length}))
}
'

How to interpret the output (what indicates a problem):

  • Any entry in “ClusterRoleBindings that grant cluster-admin” is potentially risky and must be reviewed.
  • Especially suspicious:
    • Bindings where SUBJECTS includes generic or shared identities such as:
      • Group/system:authenticated, Group/system:serviceaccounts, Group/system:masters (or similar broad groups).
      • User or group names that look like teams (devs, qa, ops, developers, etc.) instead of specific admin identities.
    • ServiceAccounts in application namespaces (e.g., default, prod, staging, app-*) rather than a dedicated admin namespace.
  • The JSON “Summary counts” at the end:
    • A total_clusterrolebindings_using_cluster_admin greater than a very small number (commonly >1–3) warrants closer review.
    • A large total_subjects or by_kind entries with many User/Group subjects indicate overuse of cluster-admin.

Use this report to decide, per binding, whether the subject truly needs full cluster-admin. If not, remove that ClusterRoleBinding and replace it with a narrower ClusterRole/RoleBinding as appropriate.