The 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
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS GKE
- 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
Remediation
Manual Steps
-
On any machine with kubectl access, list all ClusterRoleBindings that grant
cluster-adminand see who they bind to:kubectl get clusterrolebindings \-o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS_KIND:.subjects[*].kind,SUBJECTS_NAME:.subjects[*].name \| grep 'cluster-admin' -
For each binding you found, display full details (to distinguish system vs non-system subjects):
kubectl get clusterrolebinding <binding-name> -o yamlManually review each subject; plan to keep bindings only if they are:
Groupwith namesystem:masters, or- other
system:prefixed subjects that are required for core components.
-
For any non-system user/group/service account that does not strictly require
cluster-admin, design or select a least-privilegedRole/ClusterRolethat matches its actual needs. For example, to create a narrowerClusterRolefor read-only access to pods and services:cat << 'EOF' > readonly-pods-services-clusterrole.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:name: readonly-pods-servicesrules:- apiGroups: [""]resources: ["pods", "services"]verbs: ["get", "list", "watch"]EOFkubectl apply -f readonly-pods-services-clusterrole.yaml -
Bind the least-privileged role to the principal instead of
cluster-admin, adjusting kind/name/namespace as appropriate:- Example for a user:
cat << 'EOF' > readonly-binding-user.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:name: readonly-pods-services-user-alicesubjects:- kind: Username: alice@example.comroleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: readonly-pods-servicesEOFkubectl apply -f readonly-binding-user.yaml
- Example for a service account:
cat << 'EOF' > readonly-binding-sa.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:name: readonly-pods-services-sa-myappsubjects:- kind: ServiceAccountname: myappnamespace: defaultroleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: readonly-pods-servicesEOFkubectl apply -f readonly-binding-sa.yaml
- Example for a user:
-
After confirming that the replacement binding(s) exist and are correct, remove the excessive
cluster-adminbinding(s), being careful not to delete requiredsystem:bindings or thesystem:mastersmapping:kubectl delete clusterrolebinding <binding-name> -
Verification (on any machine with kubectl access): rerun the audit, which should now return
NO_CLUSTER_ADMIN_BINDINGSor only allowlist subjects:kubectl get clusterrolebindings -o json | jq -r '[.items[]| select(.roleRef.name == "cluster-admin")| .subjects[]?| select(.kind != "Group" or .name != "system:masters")]| if length == 0then "NO_CLUSTER_ADMIN_BINDINGS"else "FOUND_CLUSTER_ADMIN_BINDING"end'
Using kubectl
On any machine with kubectl access:
- List all ClusterRoleBindings that grant
cluster-adminand inspect subjects:
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name | grep cluster-admin
For deeper inspection of a specific binding:
kubectl get clusterrolebinding <binding-name> -o yaml
- For each non-system subject that does not truly require
cluster-admin, create or select a least‑privileged (Cluster)Role and bind to that instead.
Example: create a narrowly scoped ClusterRole (edit as appropriate):
cat << 'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: app-ops-readonly
rules:
- apiGroups: [""]
resources: ["pods","services","configmaps"]
verbs: ["get","list","watch"]
EOF
Bind the principal to this least‑privileged ClusterRole:
cat << 'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-ops-readonly-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: app-ops-readonly
subjects:
- kind: User
name: user@example.com
apiGroup: rbac.authorization.k8s.io
EOF
- Remove excessive
cluster-adminbindings for non-system principals (do NOT delete anysystem:bindings or thesystem:mastersgroup):
kubectl delete clusterrolebinding <binding-name>
Repeat for each binding you have replaced with a least‑privileged alternative.
- 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: Assist review of cluster-admin bindings and optionally remove non-required ones.
# Scope: Run on any machine with kubectl access and correct context.
# Notes: This is a MANUAL control. This script does NOT auto-delete anything without confirmation.
set -euo pipefail
echo "=== Discovering all ClusterRoleBindings to role 'cluster-admin' ==="
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name \
| grep 'cluster-admin' || true
echo
echo "=== Detailed JSON view of non-system 'cluster-admin' subjects (for review) ==="
kubectl get clusterrolebindings -o json | jq '
[
.items[]
| select(.roleRef.name == "cluster-admin")
| {
name: .metadata.name,
subjects: [
.subjects[]?
| select(.kind != "Group" or .name != "system:masters")
]
}
]
| map(select(.subjects | length > 0))
'
echo
echo "======================================================================"
echo "MANUAL REVIEW REQUIRED:"
echo " 1. For each binding above, decide if the subjects truly require full"
echo " cluster-admin. If not, design a least-privileged (Cluster)Role."
echo " 2. Create that Role/ClusterRole and bind subjects to it BEFORE removal."
echo " 3. Do NOT modify or delete any 'system:' prefixed bindings."
echo "======================================================================"
echo
read -r -p "List candidate ClusterRoleBindings (excluding 'system:' prefixes) for deletion? [y/N]: " LIST_CAND
if [[ "${LIST_CAND:-N}" =~ ^[Yy]$ ]]; then
echo
echo "=== Candidate ClusterRoleBindings (roleRef.name == 'cluster-admin', name does NOT start with 'system:') ==="
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| select(.metadata.name | startswith("system:") | not)
| .metadata.name
'
fi
read -r -p "Enter a ClusterRoleBinding name to DELETE (or leave blank to skip deletion): " CRB_NAME
if [[ -n "${CRB_NAME}" ]]; then
echo
echo "You are about to DELETE ClusterRoleBinding: ${CRB_NAME}"
echo "Ensure you have already created replacement least-privileged bindings as needed."
read -r -p "Type the binding name again to confirm deletion: " CRB_CONFIRM
if [[ "${CRB_CONFIRM}" == "${CRB_NAME}" ]]; then
echo "Deleting ClusterRoleBinding '${CRB_NAME}'..."
kubectl delete clusterrolebinding "${CRB_NAME}"
else
echo "Confirmation did not match. Skipping deletion."
fi
else
echo "No deletion requested. Proceeding to verification."
fi
echo
echo "=== Verification: re-running benchmark audit logic ==="
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
'