More Info:
Default service accounts should not be used by workloads and should not auto-mount tokens. Explicit, purpose-built service accounts should be created for workloads needing API access.Risk Level
MediumAddress
SecurityCompliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
Manual Steps
-
Disable token automount on all default ServiceAccounts
- Run on: any machine with kubectl access
# Patch every default ServiceAccount in every namespace kubectl get sa --all-namespaces -o json \ | jq -r '.items[] | select(.metadata.name=="default") | "\(.metadata.namespace)"' \ | sort -u \ | xargs -I{} kubectl patch sa default -n {} -p '{"automountServiceAccountToken": false}' -
Identify pods using the default ServiceAccount
- Run on: any machine with kubectl access
kubectl get pods --all-namespaces -o json \ | jq -r '.items[] | select(.spec.serviceAccountName=="default") | "\(.metadata.namespace) \(.metadata.name)"' \ | sort -
For each affected workload, create a dedicated ServiceAccount
- Run on: any machine with kubectl access
- Example for namespace
my-namespaceand appmy-app(repeat per app/namespace as needed):
kubectl create serviceaccount my-app-sa -n my-namespace -
Update workloads to use the new ServiceAccount
- Run on: any machine with kubectl access
- Example for a Deployment (repeat for DaemonSets/StatefulSets/Jobs, etc.):
Set:# Edit the workload to set spec.template.spec.serviceAccountName kubectl -n my-namespace edit deployment my-appSave and exit to trigger a rolling update.spec: template: spec: serviceAccountName: my-app-sa -
Recreate or roll pods that were using the default ServiceAccount directly
- Run on: any machine with kubectl access
- For pods managed by a controller, the edit in step 4 is enough.
- For bare pods (no controller), delete and recreate them with an explicit
serviceAccountName:
kubectl -n my-namespace delete pod POD_NAME # Recreate from manifest after adding: # spec: # serviceAccountName: my-app-sa -
Verify remediation
- Run on: any machine with kubectl access
echo "š¹ Default Service Accounts with automountServiceAccountToken enabled:" default_sa_count=$(kubectl get serviceaccounts --all-namespaces -o json | jq ' [.items[] | select(.metadata.name == "default" and (.automountServiceAccountToken != false))] | length') if [ "$default_sa_count" -gt 0 ]; then echo "default_sa_not_auto_mounted" else echo "OK: all default SAs have automountServiceAccountToken=false" fi echo "\nš¹ Pods using default ServiceAccount:" pods_using_default_sa=$(kubectl get pods --all-namespaces -o json | jq ' [.items[] | select(.spec.serviceAccountName == "default")] | length') if [ "$pods_using_default_sa" -gt 0 ]; then echo "default_sa_used_in_pods" else echo "OK: no pods are using the default ServiceAccount" fi
Using kubectl
Using kubectl
# 1) Disable token automount on all existing *default* ServiceAccounts
# Run on: any machine with kubectl access
kubectl get sa default --all-namespaces -o json | \
jq -r '.items[] |
"cat <<\"EOF\" | kubectl apply -f -\n" +
"---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: default\n namespace: " + .metadata.namespace + "\nautomountServiceAccountToken: false\nEOF\n"' | \
bash
# 2) (Recommended) Ensure new workloads use explicit, non-default ServiceAccounts
# Example: create a namespace-scoped service account for a workload
# Create a dedicated ServiceAccount in a chosen namespace (example: app-namespace)
kubectl create namespace app-namespace --dry-run=client -o yaml | kubectl apply -f -
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app-namespace
EOF
# Patch an existing deployment to use the explicit ServiceAccount (example)
kubectl -n app-namespace patch deploy my-app-deployment \
-p '{"spec":{"template":{"spec":{"serviceAccountName":"app-sa"}}}}'
# 3) Verification
# 3a) Confirm all default ServiceAccounts have automountServiceAccountToken: false
kubectl get serviceaccounts --all-namespaces -o json | jq '
[.items[] | select(.metadata.name == "default")] |
map({ns: .metadata.namespace, name: .metadata.name, automount: .automountServiceAccountToken})'
# 3b) Re-run the benchmark audit logic to ensure no failures
echo "š¹ Default Service Accounts with automountServiceAccountToken enabled:"
default_sa_count=$(kubectl get serviceaccounts --all-namespaces -o json | jq '
[.items[] | select(.metadata.name == "default" and (.automountServiceAccountToken != false))] | length')
if [ "$default_sa_count" -gt 0 ]; then
echo "default_sa_not_auto_mounted"
else
echo "OK: all default ServiceAccounts have automountServiceAccountToken=false"
fi
echo "\nš¹ Pods using default ServiceAccount:"
pods_using_default_sa=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.serviceAccountName == "default")] | length')
if [ "$pods_using_default_sa" -gt 0 ]; then
echo "default_sa_used_in_pods"
else
echo "OK: no pods using the default ServiceAccount (new pods)"
fi
Automation
Automation
#!/usr/bin/env bash
set -euo pipefail
# This script:
# 1. Sets automountServiceAccountToken: false on every "default" ServiceAccount in all namespaces.
# 2. Finds pods using the default SA and generates workload-specific ServiceAccounts for them.
# 3. Patches those workloads to use the new ServiceAccounts.
# 4. Verifies the benchmark checks pass.
#
# Requirements:
# - Run on any machine with kubectl + jq + bash.
# - kubectl must be configured with cluster-admin-like privileges.
# Fail early if required tools are missing
for bin in kubectl jq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: $bin is required but not found in PATH" >&2
exit 1
fi
done
echo "=== Step 1: Disable automountServiceAccountToken on all default ServiceAccounts ==="
# Get all namespaces
namespaces=$(kubectl get ns -o jsonpath='{.items[*].metadata.name}')
for ns in $namespaces; do
# Check if default SA exists in the namespace
if kubectl get sa default -n "$ns" >/dev/null 2>&1; then
echo "Processing default ServiceAccount in namespace: $ns"
# Patch the default SA to ensure automountServiceAccountToken: false
# This is idempotent; re-running will not cause issues.
kubectl patch sa default -n "$ns" \
--type merge \
-p '{"automountServiceAccountToken": false}' >/dev/null
fi
done
echo "=== Step 2: Create explicit ServiceAccounts for workloads using default SA and patch them ==="
# Helper: safely get kind/name from ownerReferences
get_owner_ref() {
jq -r '
.metadata.ownerReferences // [] |
map(select(.controller == true) | "\(.kind)/\(.name)") |
.[0] // ""
'
}
# Work through all pods using the default SA
pods_json=$(kubectl get pods --all-namespaces -o json)
echo "$pods_json" | jq -c '.items[] | select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null)' | while read -r pod; do
ns=$(echo "$pod" | jq -r '.metadata.namespace')
pod_name=$(echo "$pod" | jq -r '.metadata.name')
owner_ref=$(echo "$pod" | get_owner_ref)
if [[ -z "$owner_ref" ]]; then
echo "Skipping naked Pod $ns/$pod_name (no controller owner). Review manually if needed."
continue
fi
owner_kind=${owner_ref%%/*}
owner_name=${owner_ref##*/}
# We only handle common workload controllers. Others should be reviewed manually.
case "$owner_kind" in
Deployment|StatefulSet|DaemonSet|ReplicaSet|Job|CronJob)
;;
*)
echo "Skipping $owner_kind $ns/$owner_name (not an automated workload type handled by this script)."
continue
;;
esac
# Construct a deterministic ServiceAccount name per workload
sa_name="sa-${owner_kind,,}-${owner_name}"
echo "Namespace: $ns | Workload: $owner_kind/$owner_name | Pod: $pod_name | Target SA: $sa_name"
# Create the ServiceAccount if it does not exist (idempotent)
if ! kubectl get sa "$sa_name" -n "$ns" >/dev/null 2>&1; then
echo " - Creating ServiceAccount $ns/$sa_name"
kubectl create sa "$sa_name" -n "$ns" >/dev/null
else
echo " - ServiceAccount $ns/$sa_name already exists; reusing"
fi
# Patch the workload to use the explicit ServiceAccount
case "$owner_kind" in
Deployment|StatefulSet|DaemonSet|ReplicaSet|Job)
echo " - Patching $owner_kind/$owner_name to use $sa_name"
kubectl patch "$owner_kind" "$owner_name" -n "$ns" \
--type merge \
-p "{\"spec\":{\"template\":{\"spec\":{\"serviceAccountName\":\"$sa_name\",\"automountServiceAccountToken\":true}}}}" >/dev/null
;;
CronJob)
echo " - Patching CronJob/$owner_name to use $sa_name"
kubectl patch CronJob "$owner_name" -n "$ns" \
--type merge \
-p "{\"spec\":{\"jobTemplate\":{\"spec\":{\"template\":{\"spec\":{\"serviceAccountName\":\"$sa_name\",\"automountServiceAccountToken\":true}}}}}}" >/dev/null
;;
esac
done
echo "=== Step 3: Verification (matches benchmark audit) ==="
echo "š¹ Default Service Accounts with automountServiceAccountToken enabled:"
default_sa_count=$(kubectl get serviceaccounts --all-namespaces -o json | jq '
[.items[] | select(.metadata.name == "default" and (.automountServiceAccountToken != false))] | length')
if [ "$default_sa_count" -gt 0 ]; then
echo "default_sa_not_auto_mounted (remaining count: $default_sa_count)"
else
echo "OK: All default ServiceAccounts have automountServiceAccountToken set to false."
fi
echo
echo "š¹ Pods using default ServiceAccount:"
pods_using_default_sa=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null)] | length')
if [ "$pods_using_default_sa" -gt 0 ]; then
echo "default_sa_used_in_pods (remaining count: $pods_using_default_sa)"
echo "NOTE: Some pods still use the default ServiceAccount (e.g., naked Pods or uncommon controllers)."
echo " Review and update these workloads manually."
else
echo "OK: No pods are using the default ServiceAccount."
fi

