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 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'
  2. 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 ServiceAccount
    kubectl get serviceaccount -n NAMESPACE SA_NAME -o yaml

    # See which pods use it
    kubectl get pods -n NAMESPACE -o wide --field-selector spec.serviceAccountName=SA_NAME
  3. Design least-privilege RBAC for that ServiceAccount by creating a dedicated Role/ClusterRole and 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.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: SA_NAME-minimal
    namespace: NAMESPACE
    rules:
    - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: SA_NAME-minimal-binding
    namespace: NAMESPACE
    subjects:
    - kind: ServiceAccount
    name: SA_NAME
    namespace: NAMESPACE
    roleRef:
    kind: Role
    name: SA_NAME-minimal
    apiGroup: rbac.authorization.k8s.io
    EOF

    kubectl apply -f sa-least-priv-role.yaml
  4. After confirming the workload still functions with the new least-privilege Role/ClusterRole, delete the cluster-admin binding(s) that reference ServiceAccounts (run on any machine with kubectl access). For each violating ClusterRoleBinding CRB_NAME from step 1:

    kubectl delete clusterrolebinding CRB_NAME
  5. If a cluster-admin ClusterRoleBinding 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 binding
    kubectl 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
  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 cluster is compliant when the output is exactly:

    is_compliant=true
Using kubectl

On any machine with kubectl access to the cluster:

  1. 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)"
][]'
  1. 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'