Skip to main content

Ensure Cluster Admin Role Is Only Used Where Required

More Info:

The RBAC role cluster-admin provides wide-ranging powers over the environment and should be used only where and when needed.

Risk Level

High

Address

Security

Compliance Standards

  • APRA CPS 234 (Australia)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS EKS
  • CMMC 2.0
  • CSA Cloud Controls Matrix v4
  • DPDPA
  • Digital Operational Resilience Act (EU)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIS2 Directive
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework
  • UK NCSC Cyber Assessment Framework

Triage and Remediation

Remediation

Manual Steps
  1. List all clusterrolebindings that grant cluster-admin

    • Run on: any machine with kubectl access
    kubectl get clusterrolebindings -o json | jq -r '
    .items[]
    | select(.roleRef.name == "cluster-admin")
    | .metadata.name
    ' | sort -u
  2. Inspect each binding’s subjects and note which are non-system users/groups

    • Run on: any machine with kubectl access
    for crb in $(kubectl get clusterrolebindings -o json | jq -r '
    .items[]
    | select(.roleRef.name == "cluster-admin")
    | .metadata.name
    ' | sort -u); do
    echo "=== $crb ==="
    kubectl get clusterrolebinding "$crb" -o yaml
    done
    • For each binding, identify subjects where:
      • kind is not Group, or
      • kind is Group and name is neither system:masters nor system:nodes.
  3. For each subject that should not have full cluster-admin, design or select a less-privileged role

    • Run on: any machine with kubectl access
    • Example: create a namespace-scoped admin role (adjust namespace and rules as needed):
    cat << 'EOF' | kubectl apply -f -
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: limited-admin
    namespace: default
    rules:
    - apiGroups: [""]
    resources: ["pods","services","configmaps"]
    verbs: ["get","list","watch","create","update","patch","delete"]
    EOF
    • Or create a ClusterRole if cluster-wide but still reduced permissions are required.
  4. Bind affected subjects to the new lower-privilege role/clusterrole

    • Run on: any machine with kubectl access
    • Example for a Role (namespace admin):
    kubectl create rolebinding limited-admin-binding \
    --role=limited-admin \
    --user="alice@example.com" \
    --namespace=default
    • Example for a ClusterRole:
    kubectl create clusterrolebinding limited-cluster-access \
    --clusterrole=limited-cluster-role \
    --user="alice@example.com"
  5. Remove unnecessary cluster-admin bindings once replacement access is in place

    • Run on: any machine with kubectl access
    • To delete a whole binding that is no longer needed:
    kubectl delete clusterrolebinding <clusterrolebinding-name>
    • If a binding mixes system groups with non-system subjects, recreate it without the non-system subjects:
      # backup
      kubectl get clusterrolebinding <name> -o yaml > /tmp/<name>.yaml
      # edit out unwanted subjects
      sed -i '/name: alice@example.com/,+2 d' /tmp/<name>.yaml
      # reapply
      kubectl delete clusterrolebinding <name>
      kubectl apply -f /tmp/<name>.yaml
  6. Verify that only allowed subjects retain cluster-admin

    • Run on: any machine with kubectl access
    kubectl get clusterrolebindings -o json | jq -r '
    .items[]
    | select(.roleRef.name == "cluster-admin")
    | .subjects[]?
    | select(.kind != "Group" or (.name != "system:masters" and .name != "system:nodes"))
    | "FOUND_CLUSTER_ADMIN_BINDING"
    ' || echo "NO_CLUSTER_ADMIN_BINDINGS"
    • Confirm the output is NO_CLUSTER_ADMIN_BINDINGS and no non-system subjects are reported.
Using kubectl

On any machine with kubectl access:

  1. List all ClusterRoleBindings that grant cluster-admin
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| .metadata.name
' | sort -u
  1. Inspect each binding and its subjects to decide if cluster-admin is really required
    Replace <crb-name> with each name from step 1:
kubectl get clusterrolebinding <crb-name> -o yaml
  1. (Optional) Create or bind to a lower-privilege role instead of cluster-admin

Example: create a constrained ClusterRole (edit rules as appropriate for your use case), save as limited-admin-clusterrole.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: limited-admin
rules:
- apiGroups: [""]
resources: ["pods","services","configmaps","secrets"]
verbs: ["get","list","watch","create","update","patch","delete"]
- apiGroups: ["apps"]
resources: ["deployments","daemonsets","statefulsets","replicasets"]
verbs: ["get","list","watch","create","update","patch","delete"]

Apply it:

kubectl apply -f limited-admin-clusterrole.yaml

Then bind a user/group/serviceaccount to this new role, e.g. user@example.com, as limited-admin-<name>.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: limited-admin-user-example-com
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: limited-admin
subjects:
- kind: User
name: user@example.com
apiGroup: rbac.authorization.k8s.io

Apply it:

kubectl apply -f limited-admin-user-example-com.yaml
  1. Remove unnecessary cluster-admin ClusterRoleBindings

After you have confirmed that affected identities work with reduced privileges (or no access, if appropriate), delete each unnecessary binding:

kubectl delete clusterrolebinding <crb-name>

Repeat for all ClusterRoleBindings that should no longer grant cluster-admin.

  1. Verification

Run the benchmark audit command again and ensure it prints NO_CLUSTER_ADMIN_BINDINGS (or only finds the allowed system groups):

kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or (.name != "system:masters" and .name != "system:nodes"))
| "FOUND_CLUSTER_ADMIN_BINDING"
' || echo "NO_CLUSTER_ADMIN_BINDINGS"
Automation
#!/usr/bin/env bash
#
# Purpose:
# Identify and clean up risky clusterrolebindings that grant cluster-admin
# to subjects other than the default system:masters and system:nodes groups.
#
# Requirements:
# - Run on any machine with kubectl access and correct kubeconfig.
# - jq and kubectl must be installed and in PATH.
#
# Behavior:
# - For each offending ClusterRoleBinding to cluster-admin:
# * Prints the binding and its non-system subjects.
# * Prompts for confirmation before deletion (per binding).
# - Safe to re-run; deleted bindings stay deleted.
#
# NOTE:
# This script does NOT automatically create replacement lower-privilege roles.
# You must ensure that affected subjects either do not need cluster-admin
# or already have appropriate alternative bindings before confirming deletion.

set -euo pipefail

# Fail early if dependencies are missing
for bin in kubectl jq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: Required binary '$bin' not found in PATH" >&2
exit 1
fi
done

echo "Discovering ClusterRoleBindings that grant 'cluster-admin' to non-system subjects..."
echo

# Get all clusterrolebindings that reference cluster-admin
mapfile -t CRBS < <(
kubectl get clusterrolebindings -o json |
jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "cluster-admin")
| .metadata.name
' | sort -u
)

if [ "${#CRBS[@]}" -eq 0 ]; then
echo "No ClusterRoleBindings reference the 'cluster-admin' ClusterRole."
echo "Verification:"
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or (.name != "system:masters" and .name != "system:nodes"))
| "FOUND_CLUSTER_ADMIN_BINDING"
' || true
exit 0
fi

deleted_any=false

for crb in "${CRBS[@]}"; do
# Extract subjects of interest: anything except system:masters or system:nodes groups
subjects_json=$(kubectl get clusterrolebinding "$crb" -o json)
mapfile -t SUBJECTS < <(
jq -r '
.subjects[]? |
select(.kind != "Group" or (.name != "system:masters" and .name != "system:nodes")) |
"\(.kind) \(.apiGroup // "") \(.namespace // "") \(.name)"
' <<<"$subjects_json" | sed 's/ / /g'
)

# If this CRB has no non-system subjects, skip it
if [ "${#SUBJECTS[@]}" -eq 0 ]; then
continue
fi

echo "--------------------------------------------------------------------------------"
echo "ClusterRoleBinding: $crb"
echo "Grants 'cluster-admin' to the following non-system subjects:"
for s in "${SUBJECTS[@]}"; do
echo " - $s"
done
echo
echo "Per remediation guidance, these subjects should typically be bound to"
echo "less-privileged roles if they do not strictly require cluster-admin."
echo

# Confirm deletion interactively
read -r -p "Delete this ClusterRoleBinding '$crb'? [y/N]: " ans
case "$ans" in
y|Y|yes|YES)
echo "Deleting ClusterRoleBinding '$crb'..."
kubectl delete clusterrolebinding "$crb"
deleted_any=true
;;
*)
echo "Skipping deletion of '$crb'."
;;
esac
echo
done

echo "--------------------------------------------------------------------------------"
echo "Post-change verification:"
# Re-run the benchmark audit logic to prove compliance
audit_output=$(kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or (.name != "system:masters" and .name != "system:nodes"))
| "FOUND_CLUSTER_ADMIN_BINDING"
')

if [ -z "$audit_output" ]; then
echo "NO_CLUSTER_ADMIN_BINDINGS"
echo "No remaining non-system subjects are bound to 'cluster-admin'."
else
echo "$audit_output"
echo
echo "There are still ClusterRoleBindings granting 'cluster-admin' to non-system subjects."
echo "Review those bindings and re-run this script as needed."
fi

# Exit code reflects whether non-compliant bindings remain
if [ -z "$audit_output" ]; then
exit 0
else
exit 1
fi

Additional Reading: