Skip to main content

Ensure Default Service Accounts Are Not Actively Used

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​

Medium

Address​

Security

Compliance Standards​

  • CIS GKE

Triage and Remediation​

Remediation​

Manual Steps
  1. 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}'
  2. 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
  3. For each affected workload, create a dedicated ServiceAccount

    • Run on: any machine with kubectl access
    • Example for namespace my-namespace and app my-app (repeat per app/namespace as needed):
    kubectl create serviceaccount my-app-sa -n my-namespace
  4. Update workloads to use the new ServiceAccount

    • Run on: any machine with kubectl access
    • Example for a Deployment (repeat for DaemonSets/StatefulSets/Jobs, etc.):
    # Edit the workload to set spec.template.spec.serviceAccountName
    kubectl -n my-namespace edit deployment my-app

    Set:

    spec:
    template:
    spec:
    serviceAccountName: my-app-sa

    Save and exit to trigger a rolling update.

  5. 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
  6. 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
# 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
#!/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