No ServiceAccount Should Be Bound To cluster-admin
More Info:
Verifies no ServiceAccount is bound to the cluster-admin ClusterRole. Such a binding hands full cluster control to any workload using that account.
Risk Level
Critical
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
List all ClusterRoleBindings that bind
cluster-adminto any ServiceAccount (run on any machine with kubectl access):kubectl get clusterrolebindings -o json | jq -r '[ .items[]| select(.roleRef.name == "cluster-admin")| .metadata as $m| ((.subjects // [])[] | select(.kind == "ServiceAccount"))| "kind=ClusterRoleBinding name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ " sa=\(.namespace)/\(.name) roleRef=cluster-admin is_compliant=false"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end' -
For each violating ServiceAccount, determine the minimal permissions it actually needs (run on any machine with kubectl access):
- Inspect its namespace and associated workloads:
kubectl get sa -Akubectl get pods -A -o wide --field-selector spec.serviceAccountName=<service-account-name>
- Review application requirements (deployment manifests, documentation, and logs for authorization errors).
- Inspect its namespace and associated workloads:
-
Create a narrowly scoped Role or ClusterRole that grants only the required verbs and resources (run on any machine with kubectl access, then apply to the cluster):
- Example namespaced Role (adjust namespace, resources, verbs):
cat << 'EOF' > sa-limited-role.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: <workload-sa-role>namespace: <sa-namespace>rules:- apiGroups: [""]resources: ["pods"]verbs: ["get", "list"]EOFkubectl apply -f sa-limited-role.yaml
- Example RoleBinding to attach it to the ServiceAccount:
cat << 'EOF' > sa-limited-rolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: <workload-sa-rolebinding>namespace: <sa-namespace>subjects:- kind: ServiceAccountname: <service-account-name>namespace: <sa-namespace>roleRef:apiGroup: rbac.authorization.k8s.iokind: Rolename: <workload-sa-role>EOFkubectl apply -f sa-limited-rolebinding.yaml
- Example namespaced Role (adjust namespace, resources, verbs):
-
After confirming the workload functions correctly with the new, least-privilege Role/RoleBinding (monitor pod logs and behavior), identify the offending ClusterRoleBinding names (run on any machine with kubectl access):
kubectl get clusterrolebindings -o json | jq -r '.items[]| select(.roleRef.name == "cluster-admin")| select((.subjects // [])[]? | select(.kind == "ServiceAccount"))| .metadata.name' | sort -u -
Delete each ClusterRoleBinding that binds any ServiceAccount to
cluster-admin(run on any machine with kubectl access):kubectl delete clusterrolebinding <clusterrolebinding-name-1>kubectl delete clusterrolebinding <clusterrolebinding-name-2># repeat for all names from step 4 -
Verify no ServiceAccount is bound to
cluster-admin(run on any machine with kubectl access):kubectl get clusterrolebindings -o json | jq -r '[ .items[]| select(.roleRef.name == "cluster-admin")| .metadata as $m| ((.subjects // [])[] | select(.kind == "ServiceAccount"))| "kind=ClusterRoleBinding name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ " sa=\(.namespace)/\(.name) roleRef=cluster-admin is_compliant=false"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'The output must be exactly:
is_compliant=true
Using kubectl
On any machine with kubectl access:
- Identify violating ClusterRoleBindings and affected ServiceAccounts
kubectl get clusterrolebindings -o wide
kubectl get clusterrolebindings -o yaml | grep -A5 "name: cluster-admin"
- For each ClusterRoleBinding that binds a ServiceAccount to
cluster-admin, design and apply a narrowly-scoped replacement (example only; adjust rules to what the workload actually needs):
cat << 'EOF' > sa-limited-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: example-sa-limited
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
EOF
kubectl apply -f sa-limited-role.yaml
Bind the ServiceAccount to the new Role (replace NAMESPACE and SA_NAME appropriately):
cat << 'EOF' > sa-limited-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: example-sa-limited-binding
namespace: default
subjects:
- kind: ServiceAccount
name: SA_NAME
namespace: NAMESPACE
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: example-sa-limited
EOF
kubectl apply -f sa-limited-rolebinding.yaml
- Delete the insecure ClusterRoleBinding(s)
Replace BINDING_NAME with each offending ClusterRoleBinding name:
kubectl delete clusterrolebinding BINDING_NAME
- Verification
Run the benchmark audit command exactly:
kubectl get clusterrolebindings -o json | jq -r '
[ .items[]
| select(.roleRef.name == "cluster-admin")
| .metadata as $m
| ((.subjects // [])[] | select(.kind == "ServiceAccount"))
| "kind=ClusterRoleBinding name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ " sa=\(.namespace)/\(.name) roleRef=cluster-admin is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# Automation: Remove ServiceAccount bindings to cluster-admin
#
# Runs on: any machine with kubectl access and jq installed
#
# Behavior:
# - Identifies all ClusterRoleBindings that:
# * reference roleRef.name == "cluster-admin"
# * and have at least one ServiceAccount subject
# - Prints what it will delete
# - Deletes only those ClusterRoleBindings
# - Safe to re-run: deleting an already-removed binding is a no-op
# - Verifies compliance using the benchmark audit command
set -euo pipefail
echo "Checking for ClusterRoleBindings that bind ServiceAccounts to cluster-admin..."
# Capture current violating ClusterRoleBindings (names only, de-duplicated)
violating_crbs=$(kubectl get clusterrolebindings -o json | jq -r '
[ .items[]
| select(.roleRef.name == "cluster-admin")
| select(((.subjects // [])[] | select(.kind == "ServiceAccount")) | length > 0)
| .metadata.name
] | unique[]?')
if [[ -z "${violating_crbs}" ]]; then
echo "No ClusterRoleBindings with ServiceAccounts bound to cluster-admin found."
else
echo "The following ClusterRoleBindings bind ServiceAccounts to cluster-admin and will be deleted:"
printf ' %s\n' ${violating_crbs}
# Delete each violating ClusterRoleBinding
for crb in ${violating_crbs}; do
echo "Deleting ClusterRoleBinding: ${crb}"
kubectl delete clusterrolebinding "${crb}"
done
fi
echo
echo "Verification (should print 'is_compliant=true' if no violations remain):"
kubectl get clusterrolebindings -o json | jq -r '
[ .items[]
| select(.roleRef.name == "cluster-admin")
| .metadata as $m
| ((.subjects // [])[] | select(.kind == "ServiceAccount"))
| "kind=ClusterRoleBinding name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ " sa=\(.namespace)/\(.name) roleRef=cluster-admin is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'