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

Critical

Address

Security

Compliance Standards

  • CIS Kubernetes

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 wide
    kubectl get clusterrolebindings -o yaml > /tmp/clusterrolebindings-full.yaml
  2. Identify which subjects truly require cluster-admin

    • Run on: any machine with kubectl access
    • Inspect each binding that references cluster-admin and note its subjects and usage context:
    kubectl get clusterrolebindings -o yaml | \
    awk '/kind: ClusterRoleBinding/{print "---"}1' | \
    sed -n '/roleRef:/{h;:a;n;/subjects:/{p;x;p;q};ba}'
    • For each binding where roleRef.name: cluster-admin, determine (by policy/with app owners) if the service account, user, or group truly needs full cluster-wide admin or can be restricted (for example: namespace-scoped admin, read-only, or limited custom roles).
  3. Create or select a lower-privilege role for each subject that doesn’t need cluster-admin

    • Run on: any machine with kubectl access
    • To bind to the built-in admin role in a specific namespace (example team-a and user alice):
    kubectl create rolebinding alice-admin-team-a \
    --clusterrole=admin \
    --user=alice \
    --namespace=team-a
    • Or create a custom, least-privilege ClusterRole (edit rules as needed):
    cat << 'EOF' | kubectl apply -f -
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: limited-ops
    rules:
    - apiGroups: [""]
    resources: ["pods","services"]
    verbs: ["get","list","watch"]
    EOF
    • Then bind that role to the subject instead of cluster-admin (example service account app-sa in namespace team-a):
    kubectl create clusterrolebinding app-sa-limited-ops \
    --clusterrole=limited-ops \
    --serviceaccount=team-a:app-sa
  4. Carefully migrate off cluster-admin for each subject

    • Run on: any machine with kubectl access
    • For each subject currently bound to cluster-admin, ensure its new binding (from step 3) is in place, then test access (have the subject’s workload or user confirm required operations still work).
    • Example: check current bindings for a given subject before deletion (replace alice):
    kubectl get clusterrolebindings -o custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name --no-headers | \
    grep alice || true
  5. Remove unnecessary cluster-admin clusterrolebindings

    • Run on: any machine with kubectl access
    • Once a subject no longer needs cluster-admin, delete its binding. Replace <binding-name> with the exact name from step 1 or 4:
    kubectl delete clusterrolebinding <binding-name>
    • If multiple subjects share one cluster-admin binding and only some need it, recreate a narrower binding for those that still need it, and then delete the original broad binding.
  6. Verification

    • Run on: any machine with kubectl access
    • Re-run the audit logic and confirm that no non-cluster-admin role name is bound to the cluster-admin ClusterRole:
    kubectl get clusterrolebindings \
    -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[*].name --no-headers | \
    while read -r role_name role_binding subject
    do
    if [[ "${role_name}" != "cluster-admin" && "${role_binding}" == "cluster-admin" ]]; then
    is_compliant="false"
    else
    is_compliant="true"
    fi
    echo "**role_name: ${role_name} role_binding: ${role_binding} subject: ${subject} is_compliant: ${is_compliant}"
    done
    • The output should show no lines where role_binding: cluster-admin is paired with is_compliant: false.
Using kubectl

On any machine with kubectl access:

  1. List all ClusterRoleBindings that reference cluster-admin and see their subjects
kubectl get clusterrolebindings \
-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name
  1. For each non‑cluster-admin binding that grants cluster-admin, inspect details to decide if it really needs full privileges:
kubectl get clusterrolebinding <binding-name> -o yaml
  1. If a subject should have reduced privileges, first bind it to a less‑privileged ClusterRole (example: view; adjust role/subject to your needs):
  • Example manifest to create a safer ClusterRoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: view-access-someuser
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: User
name: someuser@example.com
apiGroup: rbac.authorization.k8s.io

Apply it:

kubectl apply -f view-access-someuser.yaml
  1. Once you have confirmed the replacement access works, remove the unnecessary cluster-admin binding:
kubectl delete clusterrolebinding <binding-name>

Repeat steps 2–4 for every ClusterRoleBinding where .roleRef.name == cluster-admin and the subject is not supposed to be fully privileged.

  1. Verification (same logic as the audit, to ensure no non‑cluster-admin binding grants cluster-admin):
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[*].name --no-headers | while read -r role_name role_binding subject
do
if [[ "${role_name}" != "cluster-admin" && "${role_binding}" == "cluster-admin" ]]; then
is_compliant="false"
else
is_compliant="true"
fi
echo "**role_name: ${role_name} role_binding: ${role_binding} subject: ${subject} is_compliant: ${is_compliant}"
done
Automation
#!/usr/bin/env bash
#
# Automation for: Ensure Cluster Admin Role Is Only Used Where Required (CIS Kubernetes 5.1.1)
#
# Scope: any machine with kubectl access to the cluster
#
# This script:
# 1. Lists all ClusterRoleBindings that grant cluster-admin.
# 2. For each, prompts for a decision:
# - [s]kip (default)
# - [d]elete the ClusterRoleBinding
# - [r]eplace with a lower-privilege ClusterRole (you specify the new role name)
# 3. Optionally backs up the original ClusterRoleBinding manifest before changes.
# 4. Verifies remaining cluster-admin bindings at the end (same logic as the audit).
#
# NOTE: This is a MANUAL control – you must decide whether a given subject truly
# requires cluster-admin or can be bound to a narrower role.

set -euo pipefail

# ---------- configuration ----------

# Directory for backups of modified/deleted ClusterRoleBindings
BACKUP_DIR="./clusterrolebinding_backups_$(date +%Y%m%d_%H%M%S)"

# Set to "true" to always take a backup before modifying/deleting
ALWAYS_BACKUP="true"

# kubectl context/namespace are cluster-scoped; you may override KUBECONFIG externally if needed.

# ---------- helpers ----------

require_kubectl() {
if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH. Install/configure kubectl on this machine and retry." >&2
exit 1
fi
}

ensure_backup_dir() {
if [[ "${ALWAYS_BACKUP}" == "true" ]]; then
mkdir -p "${BACKUP_DIR}"
fi
}

backup_crb() {
local name="$1"
if [[ "${ALWAYS_BACKUP}" != "true" ]]; then
return 0
fi
local file="${BACKUP_DIR}/${name}.yaml"
echo " - Backing up ClusterRoleBinding/${name} to ${file}"
kubectl get clusterrolebinding "${name}" -o yaml > "${file}"
}

replace_crb_role() {
local crb_name="$1"
local new_role="$2"

echo " - Replacing roleRef of ClusterRoleBinding/${crb_name} with ClusterRole/${new_role}"

# Patch only the roleRef fields
kubectl patch clusterrolebinding "${crb_name}" \
--type='merge' \
-p "$(cat <<EOF
{
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "${new_role}"
}
}
EOF
)"
}

delete_crb() {
local name="$1"
echo " - Deleting ClusterRoleBinding/${name}"
kubectl delete clusterrolebinding "${name}"
}

prompt_action() {
local crb_name="$1"
local role_name="$2"
local subjects="$3"

echo
echo "ClusterRoleBinding: ${crb_name}"
echo " roleRef.name : ${role_name}"
echo " subjects : ${subjects:-<none>}"
echo
echo "Decision for ClusterRoleBinding/${crb_name}:"
echo " [s] Skip (do nothing)"
echo " [d] Delete (remove cluster-admin binding entirely)"
echo " [r] Replace with a lower-privilege ClusterRole"
read -r -p "Choose action [s/d/r] (default: s): " action

action="${action:-s}"

case "${action}" in
s|S)
echo " -> Skipping ClusterRoleBinding/${crb_name}"
;;
d|D)
backup_crb "${crb_name}"
delete_crb "${crb_name}"
;;
r|R)
read -r -p " Enter LOWER-PRIVILEGE ClusterRole name to bind instead of cluster-admin: " new_role
if [[ -z "${new_role}" ]]; then
echo " ! No role entered, skipping replacement for ClusterRoleBinding/${crb_name}"
return
fi

# Basic existence check for the new ClusterRole
if ! kubectl get clusterrole "${new_role}" >/dev/null 2>&1; then
echo " ! ClusterRole/${new_role} does not exist. Create it first or choose another role. Skipping."
return
fi

backup_crb "${crb_name}"
replace_crb_role "${crb_name}" "${new_role}"
;;
*)
echo " ! Invalid choice '${action}', skipping ClusterRoleBinding/${crb_name}"
;;
esac
}

# ---------- main logic ----------

require_kubectl
ensure_backup_dir

echo "Discovering ClusterRoleBindings that grant 'cluster-admin'..."

# List all ClusterRoleBindings where roleRef.name == cluster-admin
mapfile -t CRB_LINES < <(
kubectl get clusterrolebindings \
-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name \
--no-headers | awk '$2=="cluster-admin"'
)

if [[ "${#CRB_LINES[@]}" -eq 0 ]]; then
echo "No ClusterRoleBindings currently grant the 'cluster-admin' ClusterRole."
else
echo "The following ClusterRoleBindings grant 'cluster-admin':"
printf ' %s\n' "${CRB_LINES[@]}"
fi

for line in "${CRB_LINES[@]}"; do
# Expect: NAME ROLE SUBJECTS...
# NAME may not contain spaces; ROLE is 'cluster-admin'; SUBJECTS may be empty or space-separated.
crb_name=$(awk '{print $1}' <<<"${line}")
role_name=$(awk '{print $2}' <<<"${line}")
# Extract everything from the 3rd field onward as subjects
subjects=$(cut -d' ' -f3- <<<"${line}" || true)

prompt_action "${crb_name}" "${role_name}" "${subjects}"
done

# ---------- verification (re-run audit-style logic) ----------

echo
echo "Verification: remaining ClusterRoleBindings and compliance status:"
kubectl get clusterrolebindings \
-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[*].name \
--no-headers | while read -r role_name role_binding subject; do
if [[ "${role_name}" != "cluster-admin" && "${role_binding}" == "cluster-admin" ]]; then
is_compliant="false"
else
is_compliant="true"
fi
echo "**role_name: ${role_name} role_binding: ${role_binding} subject: ${subject} is_compliant: ${is_compliant}"
done

echo
echo "Completed. Re-run this script anytime after RBAC changes; it is safe to re-run."

Additional Reading: