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
Remediation
Manual Steps
-
List all clusterrolebindings that grant
cluster-admin- Run on: any machine with kubectl access
kubectl get clusterrolebindings -o widekubectl get clusterrolebindings -o yaml > /tmp/clusterrolebindings-full.yaml -
Identify which subjects truly require
cluster-admin- Run on: any machine with kubectl access
- Inspect each binding that references
cluster-adminand 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).
-
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
adminrole in a specific namespace (exampleteam-aand useralice):
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/v1kind: ClusterRolemetadata:name: limited-opsrules:- apiGroups: [""]resources: ["pods","services"]verbs: ["get","list","watch"]EOF- Then bind that role to the subject instead of
cluster-admin(example service accountapp-sain namespaceteam-a):
kubectl create clusterrolebinding app-sa-limited-ops \--clusterrole=limited-ops \--serviceaccount=team-a:app-sa -
Carefully migrate off
cluster-adminfor 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 -
Remove unnecessary
cluster-adminclusterrolebindings- 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-adminbinding and only some need it, recreate a narrower binding for those that still need it, and then delete the original broad binding.
-
Verification
- Run on: any machine with kubectl access
- Re-run the audit logic and confirm that no non-
cluster-adminrole name is bound to thecluster-adminClusterRole:
kubectl get clusterrolebindings \-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[*].name --no-headers | \while read -r role_name role_binding subjectdoif [[ "${role_name}" != "cluster-admin" && "${role_binding}" == "cluster-admin" ]]; thenis_compliant="false"elseis_compliant="true"fiecho "**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-adminis paired withis_compliant: false.
Using kubectl
On any machine with kubectl access:
- List all ClusterRoleBindings that reference
cluster-adminand see their subjects
kubectl get clusterrolebindings \
-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name
- For each non‑
cluster-adminbinding that grantscluster-admin, inspect details to decide if it really needs full privileges:
kubectl get clusterrolebinding <binding-name> -o yaml
- 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
- Once you have confirmed the replacement access works, remove the unnecessary
cluster-adminbinding:
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.
- Verification (same logic as the audit, to ensure no non‑
cluster-adminbinding grantscluster-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."