Skip to main content

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

Manual Steps
  1. List all ClusterRoleBindings that bind cluster-admin to 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'
  2. 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 -A
      kubectl get pods -A -o wide --field-selector spec.serviceAccountName=<service-account-name>
    • Review application requirements (deployment manifests, documentation, and logs for authorization errors).
  3. 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.yaml
      apiVersion: rbac.authorization.k8s.io/v1
      kind: Role
      metadata:
      name: <workload-sa-role>
      namespace: <sa-namespace>
      rules:
      - apiGroups: [""]
      resources: ["pods"]
      verbs: ["get", "list"]
      EOF

      kubectl apply -f sa-limited-role.yaml
    • Example RoleBinding to attach it to the ServiceAccount:
      cat << 'EOF' > sa-limited-rolebinding.yaml
      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      metadata:
      name: <workload-sa-rolebinding>
      namespace: <sa-namespace>
      subjects:
      - kind: ServiceAccount
      name: <service-account-name>
      namespace: <sa-namespace>
      roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: <workload-sa-role>
      EOF

      kubectl apply -f sa-limited-rolebinding.yaml
  4. 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
  5. 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
  6. 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:

  1. Identify violating ClusterRoleBindings and affected ServiceAccounts
kubectl get clusterrolebindings -o wide
kubectl get clusterrolebindings -o yaml | grep -A5 "name: cluster-admin"
  1. 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
  1. Delete the insecure ClusterRoleBinding(s)

Replace BINDING_NAME with each offending ClusterRoleBinding name:

kubectl delete clusterrolebinding BINDING_NAME
  1. 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'