Skip to main content

Ensure Default Service Accounts Are Not Actively Used

More Info:

Default service accounts should not be granted permissions or used by workloads. Their tokens should not be auto-mounted.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. Identify all default service accounts and their automount setting

    • Run on: any machine with kubectl access
    kubectl get serviceaccount --all-namespaces --field-selector metadata.name=default -o wide
  2. Review workloads currently using default service accounts and decide whether to change them

    • Run on: any machine with kubectl access
    # Pods explicitly or implicitly using the default service account in each namespace
    for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
    echo "=== Namespace: $ns ==="
    kubectl get pods -n "$ns" -o wide --field-selector spec.serviceAccountName=default || true
    done
    • For any pod that is not a system or vendor-managed component and still uses the default service account, plan to create and assign a dedicated ServiceAccount with only the permissions it needs.
  3. Create explicit service accounts for affected workloads (per namespace)

    • Run on: any machine with kubectl access
    • Example for one namespace (replace NAMESPACE and APP-SA with your values):
    kubectl create serviceaccount app-sa -n NAMESPACE
    • Update the corresponding RBAC (Roles/ClusterRoles and RoleBindings/ClusterRoleBindings) to grant only the minimum required permissions to app-sa. For example:
    kubectl create rolebinding app-sa-view \
    --clusterrole=view \
    --serviceaccount=NAMESPACE:app-sa \
    -n NAMESPACE
  4. Update workloads to stop using the default service account

    • Run on: any machine with kubectl access
    • Edit each affected workload (Deployment/StatefulSet/DaemonSet/CronJob/Job/Pod) to use the new explicit service account, e.g.:
    kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME
    • Under spec.template.spec, set:
    serviceAccountName: app-sa
    • Save and exit the editor; Kubernetes will roll out updated pods using the explicit service account.
  5. Disable token automount on default service accounts

    • Run on: any machine with kubectl access
    • For each namespace that has a default service account:
    NAMESPACE=example-namespace

    kubectl -n "$NAMESPACE" patch serviceaccount default \
    -p '{"automountServiceAccountToken": false}'
    • Repeat for all namespaces where you want to prevent automatic token mounting on the default service account.
  6. Verify that default service accounts are no longer actively used and have automount disabled

    • Run on: any machine with kubectl access
    # Check automountServiceAccountToken on all default service accounts
    kubectl get serviceaccount --all-namespaces --field-selector metadata.name=default -o=json \
    | jq -r '.items[] | "namespace: \(.metadata.namespace), kind: \(.kind), name: \(.metadata.name), automountServiceAccountToken: \(.automountServiceAccountToken | if . == null then "notset" else . end )"'

    # Confirm that no non-system pods are still using the default service account
    for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
    echo "=== Namespace: $ns ==="
    kubectl get pods -n "$ns" -o json \
    | jq -r '.items[] | select(.spec.serviceAccountName=="default") | .metadata.name'
    done
Using kubectl

On any machine with kubectl access:

  1. Export all existing default ServiceAccounts to manifests (for review and backup)
kubectl get serviceaccount default --all-namespaces -o yaml > /tmp/default-serviceaccounts-backup.yaml
  1. Patch all existing default ServiceAccounts to disable token auto-mount
kubectl get serviceaccount --all-namespaces \
--field-selector metadata.name=default \
-o json | \
kubectl patch -f - \
--type merge \
-p '{"automountServiceAccountToken": false}'
  1. (Optional) Enforce the setting declaratively for a specific namespace

Example manifest (save as sa-default-patch.yaml):

apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: your-namespace
automountServiceAccountToken: false

Apply it:

kubectl apply -f sa-default-patch.yaml
  1. Ensure new or existing workloads do not rely on the default ServiceAccount

For each deployment/statefulset/cronjob/etc., set an explicit non-default ServiceAccount and (optionally) disable auto-mount at pod level. Example manifest snippet:

apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: your-namespace
spec:
template:
spec:
serviceAccountName: example-sa
automountServiceAccountToken: false

Apply:

kubectl apply -f example-deployment.yaml
  1. Verification
kubectl get serviceaccount --all-namespaces \
--field-selector metadata.name=default -o=json | \
jq -r '.items[] | " namespace: \(.metadata.namespace), kind: \(.kind), name: \(.metadata.name), automountServiceAccountToken: \(.automountServiceAccountToken | if . == null then "notset" else . end )"' | \
xargs -L 1
Automation
#!/usr/bin/env bash
set -euo pipefail

# This script must run on any machine with kubectl and jq configured for the target cluster.

echo "[INFO] Discovering all 'default' ServiceAccounts in the cluster..."
mapfile -t DEFAULT_SAS < <(
kubectl get serviceaccount --all-namespaces \
--field-selector metadata.name=default \
-o json | jq -r '.items[] | [.metadata.namespace, .metadata.name] | @tsv'
)

if [ "${#DEFAULT_SAS[@]}" -eq 0 ]; then
echo "[INFO] No 'default' ServiceAccounts found."
else
echo "[INFO] Found ${#DEFAULT_SAS[@]} 'default' ServiceAccount(s)."
fi

for entry in "${DEFAULT_SAS[@]}"; do
ns=$(echo "$entry" | awk '{print $1}')
name=$(echo "$entry" | awk '{print $2}')

echo "[INFO] Processing ServiceAccount '$name' in namespace '$ns'..."

# Dump current SA as YAML
tmpfile=$(mktemp)
kubectl get serviceaccount "$name" -n "$ns" -o yaml > "$tmpfile"

# Check current automountServiceAccountToken value
current=$(yq -r '.automountServiceAccountToken // "null"' "$tmpfile")

if [ "$current" = "false" ]; then
echo "[INFO] automountServiceAccountToken already false; no change needed."
rm -f "$tmpfile"
continue
fi

# Ensure top-level automountServiceAccountToken: false is set
# yq v4 syntax
yq -y '.automountServiceAccountToken = false' "$tmpfile" > "${tmpfile}.patched"

echo "[INFO] Applying patch to set automountServiceAccountToken: false ..."
kubectl apply -f "${tmpfile}.patched"

rm -f "$tmpfile" "${tmpfile}.patched"
done

echo "[INFO] Verifying all 'default' ServiceAccounts have automountServiceAccountToken=false ..."

kubectl get serviceaccount --all-namespaces \
--field-selector metadata.name=default \
-o json | jq -r '
.items[] |
"namespace=\(.metadata.namespace) name=\(.metadata.name) automountServiceAccountToken=\(.automountServiceAccountToken // "notset")"
' | sed 's/^/[RESULT] /'

# Fail if any default SA is still not set to false
non_compliant_count=$(
kubectl get serviceaccount --all-namespaces \
--field-selector metadata.name=default \
-o json | jq '[.items[] | select((.automountServiceAccountToken // false) != false)] | length'
)

if [ "$non_compliant_count" -ne 0 ]; then
echo "[ERROR] $non_compliant_count 'default' ServiceAccount(s) still do not have automountServiceAccountToken=false."
exit 1
fi

echo "[INFO] All 'default' ServiceAccounts now have automountServiceAccountToken=false."