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 ServiceAccounts currently 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' -
For each violating ServiceAccount, identify what access it truly needs by checking its namespace, name, and associated workloads (run on any machine with kubectl access):
# Example: inspect one violating ServiceAccountkubectl get serviceaccount -n NAMESPACE SA_NAME -o yaml# See which pods use itkubectl get pods -n NAMESPACE -o wide --field-selector spec.serviceAccountName=SA_NAME -
Design least-privilege RBAC for that ServiceAccount by creating a dedicated
Role/ClusterRoleand binding (run on any machine with kubectl access). Example template (edit apiGroups, resources, verbs, and scope as required, then apply):cat << 'EOF' > sa-least-priv-role.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: SA_NAME-minimalnamespace: NAMESPACErules:- apiGroups: [""]resources: ["pods"]verbs: ["get", "list"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: SA_NAME-minimal-bindingnamespace: NAMESPACEsubjects:- kind: ServiceAccountname: SA_NAMEnamespace: NAMESPACEroleRef:kind: Rolename: SA_NAME-minimalapiGroup: rbac.authorization.k8s.ioEOFkubectl apply -f sa-least-priv-role.yaml -
After confirming the workload still functions with the new least-privilege Role/ClusterRole, delete the
cluster-adminbinding(s) that reference ServiceAccounts (run on any machine with kubectl access). For each violating ClusterRoleBindingCRB_NAMEfrom step 1:kubectl delete clusterrolebinding CRB_NAME -
If a
cluster-adminClusterRoleBinding also includes non-ServiceAccount subjects (e.g., users/groups) that must be retained, recreate an equivalent binding without the ServiceAccounts before deleting the original (run on any machine with kubectl access):# Export the existing bindingkubectl get clusterrolebinding CRB_NAME -o yaml > crb-original.yaml# Edit crb-original.yaml: remove all subjects with kind: ServiceAccount, keep only required User/Group subjects# and ensure roleRef.name: cluster-admin is unchanged.kubectl apply -f crb-original.yaml# Then remove any remaining ServiceAccount-only bindings as in 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 cluster is compliant when the output is exactly:
is_compliant=true
Using kubectl
On any machine with kubectl access to the cluster:
- List ServiceAccounts bound to
cluster-admin:
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) sa=\(.namespace)/\(.name)"
][]'
- For each violating ClusterRoleBinding, delete it. Example (replace the name with each one you found):
kubectl delete clusterrolebinding <clusterrolebinding-name>
If you need to recreate least-privilege access, first define a narrowed ClusterRole and binding (example pattern, adjust rules to actual needs):
# save as sa-limited-access.yaml and edit rules/subjects appropriately
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-workload-limited
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-workload-limited-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: my-workload-limited
subjects:
- kind: ServiceAccount
name: my-workload-sa
namespace: my-namespace
Apply the least-privilege RBAC:
kubectl apply -f sa-limited-access.yaml
Verification (cluster is compliant when this prints is_compliant=true):
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
#
# Remediation for: "No ServiceAccount Should Be Bound To cluster-admin"
# Scope: any machine with kubectl access to the OKE cluster
#
# Behavior:
# - Detects ClusterRoleBindings that bind any ServiceAccount to cluster-admin
# - For each such binding:
# * Prints the ServiceAccounts it affects
# * Deletes the ClusterRoleBinding
# - Safe to re-run: only current violating bindings are deleted
# - Verifies compliance at the end using the provided audit logic
#
# Prerequisites:
# - kubectl configured to talk to the target OKE cluster
# - jq installed
set -euo pipefail
echo "=== Detecting ClusterRoleBindings that bind ServiceAccounts to cluster-admin ==="
# Capture all violating ClusterRoleBindings (names only, unique)
mapfile -t CRBS_TO_DELETE < <(
kubectl get clusterrolebindings -o json | jq -r '
[ .items[]
| select(.roleRef.name == "cluster-admin")
| select((.subjects // [])[]? | .kind == "ServiceAccount")
| .metadata.name
] | unique[]?'
)
if [ "${#CRBS_TO_DELETE[@]}" -eq 0 ]; then
echo "No ClusterRoleBindings bind ServiceAccounts to cluster-admin."
else
echo "Found ${#CRBS_TO_DELETE[@]} violating ClusterRoleBinding(s):"
printf ' - %s\n' "${CRBS_TO_DELETE[@]}"
echo
for crb in "${CRBS_TO_DELETE[@]}"; do
echo "=== Details for ClusterRoleBinding: ${crb} ==="
# List only ServiceAccount subjects for visibility before deletion
kubectl get clusterrolebinding "${crb}" -o json | jq -r '
.subjects // [] | map(select(.kind == "ServiceAccount"))[]
| " SA: \(.namespace)/\(.name)"'
echo "Deleting ClusterRoleBinding ${crb} ..."
kubectl delete clusterrolebinding "${crb}"
echo
done
fi
echo "=== Verification (post-remediation audit) ==="
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'