Skip to main content

Ensure Cluster-Admin Role Is Only Used Where Required

More Info:​

The cluster-admin ClusterRole grants superuser access over the entire cluster. Bindings to it should be restricted to the minimum set of principals that strictly require it.

Risk Level​

Critical

Address​

Security

Compliance Standards​

  • CIS GKE

Triage and Remediation​

Remediation​

Manual Steps
  1. On any machine with kubectl access, list all ClusterRoleBindings that use the cluster-admin role and see their subjects:

    kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name | grep cluster-admin
  2. For each binding identified in step 1, inspect full details (including subject kinds and namespaces):

    kubectl get clusterrolebinding <binding-name> -o yaml

    Manually determine whether each non-system subject (Users, Groups, ServiceAccounts) truly requires cluster-wide superuser rights. Do not modify bindings whose name or subjects are system-prefixed (e.g., starting with system:) and are required for core components.

  3. For each subject that does not strictly require cluster-admin, design or select a least-privileged Role/ClusterRole that covers only the permissions it needs. For example, create a custom ClusterRole on any machine with kubectl access:

    cat << 'EOF' | kubectl apply -f -
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: limited-admin-example
    rules:
    - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
    EOF
  4. Rebind each affected subject to the least-privileged (Cluster)Role instead of cluster-admin, using a new (Cluster)RoleBinding. Example for a ClusterRole and a user:

    cat << 'EOF' | kubectl apply -f -
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
    name: limited-admin-example-binding
    subjects:
    - kind: User
    name: alice@example.com
    apiGroup: rbac.authorization.k8s.io
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: ClusterRole
    name: limited-admin-example
    EOF
  5. After confirming that alternative bindings are in place and tested for each subject, remove the excessive cluster-admin bindings that are no longer required:

    kubectl delete clusterrolebinding <binding-name>
  6. Verify that no non-system principals remain bound to cluster-admin, 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")
    ]
    | if length == 0
    then "NO_CLUSTER_ADMIN_BINDINGS"
    else "FOUND_CLUSTER_ADMIN_BINDING"
    end
    '
Using kubectl

On any machine with kubectl access:

  1. List all ClusterRoleBindings that use cluster-admin and review subjects
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name \
| grep 'cluster-admin'
  1. For each non‑system binding you determine does NOT strictly require cluster-admin, remove the binding
    (Example using a placeholder name; replace with the real binding name you decided to remove.)
kubectl delete clusterrolebinding <binding-name-to-remove>
  1. (Optional) Create a least‑privilege ClusterRole and bind it instead of cluster-admin
    Example: read‑only access to pods in all namespaces.

Create a manifest file, e.g. readonly-pods-clusterrole.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: readonly-pods
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]

Apply it:

kubectl apply -f readonly-pods-clusterrole.yaml

Then bind it to the principal that previously had cluster-admin, for example a user:

kubectl create clusterrolebinding readonly-pods-binding \
--clusterrole=readonly-pods \
--user=<user-identifier>

(or use --group or --serviceaccount <namespace>:<sa-name> as appropriate.)

  1. Verification (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")
]
| if length == 0
then "NO_CLUSTER_ADMIN_BINDINGS"
else "FOUND_CLUSTER_ADMIN_BINDING"
end
'
Automation
#!/usr/bin/env bash
#
# Purpose:
# Audit and interactively clean up non-system ClusterRoleBindings
# that grant the "cluster-admin" ClusterRole.
#
# Scope:
# Run on any machine with kubectl access to the cluster and
# appropriate permissions to list and delete ClusterRoleBindings.
#
# Idempotency:
# - Read‑only until you confirm deletions.
# - Deleting an already‑deleted binding is not attempted.
# - Safe to re‑run; it will only act on currently existing bindings.
#
# IMPORTANT:
# - This script CANNOT know which principals truly require cluster-admin.
# - For each non‑system binding it shows details and asks you to decide.
# - Before confirming deletion, ensure an appropriate least‑privilege
# Role/ClusterRoleBinding has been created separately.

set -euo pipefail

# Ensure needed tools are present
for cmd in kubectl jq; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: Required command '$cmd' not found in PATH." >&2
exit 1
fi
done

echo "=== Step 1: Discover ClusterRoleBindings to 'cluster-admin' ==="

# List all ClusterRoleBindings that reference cluster-admin
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| .metadata.name
' | sort -u > /tmp/clusteradmin_bindings.$$ || true

if [[ ! -s /tmp/clusteradmin_bindings.$$ ]]; then
echo "No ClusterRoleBindings found that reference ClusterRole 'cluster-admin'."
rm -f /tmp/clusteradmin_bindings.$$
exit 0
fi

echo "ClusterRoleBindings referencing 'cluster-admin':"
cat /tmp/clusteradmin_bindings.$$
echo

echo "=== Step 2: Exclude required system bindings (system:*) from modification ==="
echo "NOTE: Bindings whose name starts with 'system:' will be SKIPPED."

to_review=()

while read -r crb; do
[[ -z "$crb" ]] && continue

if [[ "$crb" == system:* ]]; then
echo "Skipping system binding: $crb"
continue
fi

to_review+=("$crb")
done < /tmp/clusteradmin_bindings.$$

if [[ ${#to_review[@]} -eq 0 ]]; then
echo
echo "All ClusterRoleBindings to cluster-admin are system:* bindings."
echo "No non-system principals to remediate."
rm -f /tmp/clusteradmin_bindings.$$
exit 0
fi

echo
echo "Non-system ClusterRoleBindings to review:"
for crb in "${to_review[@]}"; do
echo " - $crb"
done
echo

echo "=== Step 3: Review each non-system binding and decide whether to delete ==="
echo "For each binding you will see its subjects. You must decide if it should"
echo "continue to have cluster-admin, or be removed (after ensuring a suitable"
echo "least-privilege Role/ClusterRole exists and is bound)."
echo

deleted=()

for crb in "${to_review[@]}"; do
echo "------------------------------------------------------------"
echo "Binding: $crb"
echo

# Show full YAML for detailed inspection
kubectl get clusterrolebinding "$crb" -o yaml

echo
echo "This binding grants ClusterRole 'cluster-admin' to the above subjects."
echo "If these subjects do NOT strictly require cluster-admin, you should:"
echo " 1) Create/assign a more limited Role/ClusterRoleBinding separately."
echo " 2) Then remove this excessive binding."
echo

# Prompt for action
while true; do
read -r -p "Delete this ClusterRoleBinding '$crb'? [y/N] " ans
ans=${ans:-n}
case "$ans" in
[Yy]*)
echo "Deleting ClusterRoleBinding '$crb'..."
if kubectl delete clusterrolebinding "$crb"; then
deleted+=("$crb")
else
echo "WARN: Failed to delete ClusterRoleBinding '$crb' (it may already be gone or you lack permissions)." >&2
fi
break
;;
[Nn]*)
echo "Keeping ClusterRoleBinding '$crb'."
break
;;
*)
echo "Please answer y or n."
;;
esac
done
echo
done

rm -f /tmp/clusteradmin_bindings.$$

echo "=== Step 4: Verification (re-run audit logic) ==="
echo "Running the benchmark audit expression to confirm remaining bindings..."

audit_result="$(
kubectl get clusterrolebindings -o json | jq -r '
[
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or .name != "system:masters")
]
| if length == 0
then "NO_CLUSTER_ADMIN_BINDINGS"
else "FOUND_CLUSTER_ADMIN_BINDING"
end
'
)"

echo "Audit result: $audit_result"
echo

if [[ "$audit_result" == "NO_CLUSTER_ADMIN_BINDINGS" ]]; then
echo "SUCCESS: No non-system principals are currently bound to ClusterRole 'cluster-admin'."
else
echo "NOTICE: There are still non-system principals bound to 'cluster-admin'."
echo "Review remaining ClusterRoleBindings and repeat this script as needed."
fi

if [[ ${#deleted[@]} -gt 0 ]]; then
echo
echo "Bindings deleted in this run:"
for d in "${deleted[@]}"; do
echo " - $d"
done
fi