Skip to main content

Service Account Tokens Are Only Mounted Where Necessary

More Info:

Automatically mounting service account tokens into pods that do not need API access widens the attack surface. Set automountServiceAccountToken to false unless the workload requires API server access.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS EKS

Triage and Remediation

Remediation

Manual Steps
  1. List service accounts and identify which workloads truly need API access (run on any machine with kubectl access):

    kubectl get sa -A -o wide

    For each service account in application namespaces, confirm with the application owner whether the pod needs to talk to the Kubernetes API. Only those should keep automountServiceAccountToken: true or default behavior.

  2. For a service account that does NOT need API access, patch it to disable token mounting (run on any machine with kubectl access, replace NAMESPACE and SA_NAME):

    kubectl patch sa SA_NAME -n NAMESPACE -p '{"automountServiceAccountToken": false}'
  3. For pods that explicitly do NOT need API access but still rely on the default service account, create a dedicated “no-api” service account and disable token mounting on it (run on any machine with kubectl access, replace NAMESPACE and SA_NAME):

    kubectl create sa SA_NAME -n NAMESPACE
    kubectl patch sa SA_NAME -n NAMESPACE -p '{"automountServiceAccountToken": false}'

    Then, update the pod/deployment/daemonset/statefulset spec to use this service account:

    kubectl patch deployment DEPLOYMENT_NAME -n NAMESPACE \
    --type='strategic' \
    -p '{"spec":{"template":{"spec":{"serviceAccountName":"SA_NAME"}}}}'
  4. For workloads that DO require API access, leave their service accounts as-is, but ensure least privilege by checking and minimizing their RBAC bindings (run on any machine with kubectl access, replace NAMESPACE and SA_NAME):

    kubectl get rolebinding,clusterrolebinding -A \
    -o jsonpath='{range .items[?(@.subjects[*].name=="SA_NAME")]}{.kind}{" "}{.metadata.name}{" in "}{.metadata.namespace}{"\n"}{end}'

    Adjust Role/ClusterRole and bindings to grant only the necessary verbs and resources.

  5. For any pod spec that explicitly sets automountServiceAccountToken: true but does NOT need API access, update it to false (run on any machine with kubectl access, replace kind/name/namespace):

    kubectl get deployment DEPLOYMENT_NAME -n NAMESPACE -o yaml > /tmp/deploy.yaml
    # Edit /tmp/deploy.yaml: under spec.template.spec set:
    # automountServiceAccountToken: false
    kubectl apply -f /tmp/deploy.yaml
  6. Verify that only pods which truly need API access are mounting tokens (run on any machine with kubectl access):

    pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
    [.items[] | select(.spec.automountServiceAccountToken != false)] | length')

    echo "Pods with automountServiceAccountToken not set to false: $pods_with_token_mount"
Using kubectl

On any machine with kubectl access:

  1. Identify pods that should not have the token mounted but do
kubectl get pods --all-namespaces -o wide

Manually determine which of these workloads do not need Kubernetes API access.

  1. For each workload that does NOT need API access, edit its Pod spec to disable the token mount. Prefer editing the owning controller (Deployment, DaemonSet, StatefulSet, Job, CronJob) rather than the live Pod.

Example: patch a Deployment’s pod template

kubectl -n NAMESPACE patch deployment DEPLOYMENT_NAME \
--type merge \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

Example: patch a ServiceAccount so that any pods using it do NOT auto‑mount the token

kubectl -n NAMESPACE patch serviceaccount SERVICEACCOUNT_NAME \
--type merge \
-p '{"automountServiceAccountToken":false}'
  1. Declarative manifest snippets (for future or GitOps use)

In a ServiceAccount manifest:

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

In a controller’s Pod template (e.g., Deployment):

apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
automountServiceAccountToken: false
serviceAccountName: example-sa
containers:
- name: example
image: nginx:stable

Apply:

kubectl apply -f PATH/TO/MANIFEST.yaml
  1. Verification (same logic as the audit)
pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.automountServiceAccountToken != false)] | length')

echo "$pods_with_token_mount pods with automountServiceAccountToken not explicitly set to false"
Automation
#!/usr/bin/env bash
#
# Disable automountServiceAccountToken on ServiceAccounts and Pods that
# do NOT require API server access by default, while allowing explicit
# opt-in where needed.
#
# Usage: run on any machine with kubectl, jq, and bash installed, with
# sufficient RBAC to patch pods and serviceaccounts cluster-wide.
#
# Idempotent: safe to re-run; only updates objects that are not already
# configured as desired.

set -euo pipefail

# Namespace label key that explicitly marks workloads needing API access.
# Any namespace with this label set to "true" is skipped.
NEEDS_API_LABEL_KEY="security.kubernetes.io/needs-api-access"

# Annotation key on Pods and ServiceAccounts for opt-out/opt-in control.
# Values:
# "true" -> requires API access (do NOT disable automount)
# "false" -> does NOT require API access (force disable automount)
# Annotation on Pod takes precedence over ServiceAccount if both set.
NEEDS_API_ANNOTATION_KEY="security.kubernetes.io/needs-api-access"

echo "=== Step 1: Discovering Pods and ServiceAccounts currently mounting tokens ==="

pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.automountServiceAccountToken != false)] | length')

echo "Pods with automountServiceAccountToken not explicitly set to false: ${pods_with_token_mount}"

echo "=== Step 2: Disable automountServiceAccountToken on ServiceAccounts ==="

# Get all ServiceAccounts in all namespaces
kubectl get sa --all-namespaces -o json | jq -r '.items[] |
[.metadata.namespace, .metadata.name] | @tsv' | while IFS=$'\t' read -r ns sa; do

# Skip namespaces explicitly marked as needing API access
ns_needs_api=$(kubectl get ns "${ns}" -o jsonpath="{.metadata.labels.${NEEDS_API_LABEL_KEY}}" 2>/dev/null || true)
if [ "${ns_needs_api}" = "true" ]; then
continue
fi

# Check annotation on the ServiceAccount
sa_needs_api=$(kubectl get sa "${sa}" -n "${ns}" -o jsonpath="{.metadata.annotations.${NEEDS_API_ANNOTATION_KEY}}" 2>/dev/null || true)

# If explicitly annotated "true", skip; if annotated "false" or unset, disable automount
if [ "${sa_needs_api}" = "true" ]; then
continue
fi

# Get current value to keep idempotent
current=$(kubectl get sa "${sa}" -n "${ns}" -o jsonpath="{.automountServiceAccountToken}" 2>/dev/null || true)
if [ "${current}" = "false" ]; then
continue
fi

echo "Patching ServiceAccount ${ns}/${sa} to set automountServiceAccountToken=false"
kubectl patch sa "${sa}" -n "${ns}" --type='merge' -p '{
"automountServiceAccountToken": false
}' >/dev/null
done

echo "=== Step 3: Disable automountServiceAccountToken on existing Pods where safe ==="

# Process all pods that do not already have automountServiceAccountToken=false
kubectl get pods --all-namespaces -o json | jq -r '
.items[]
| select(.spec.automountServiceAccountToken != false)
| [.metadata.namespace, .metadata.name]
| @tsv' | while IFS=$'\t' read -r ns pod; do

# Skip namespaces explicitly marked as needing API access
ns_needs_api=$(kubectl get ns "${ns}" -o jsonpath="{.metadata.labels.${NEEDS_API_LABEL_KEY}}" 2>/dev/null || true)
if [ "${ns_needs_api}" = "true" ]; then
continue
fi

# Skip pods that are terminating
phase=$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.status.phase}' 2>/dev/null || true)
if [ "${phase}" = "Succeeded" ] || [ "${phase}" = "Failed" ]; then
continue
fi

# Check per-pod annotation
pod_needs_api=$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath="{.metadata.annotations.${NEEDS_API_ANNOTATION_KEY}}" 2>/dev/null || true)
if [ "${pod_needs_api}" = "true" ]; then
continue
fi

# If pod annotation is "false", we will disable automount.
# If pod annotation is unset, check its ServiceAccount annotation.
if [ "${pod_needs_api}" != "false" ]; then
sa_name=$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.spec.serviceAccountName}' 2>/dev/null || true)
if [ -n "${sa_name}" ]; then
sa_needs_api=$(kubectl get sa "${sa_name}" -n "${ns}" -o jsonpath="{.metadata.annotations.${NEEDS_API_ANNOTATION_KEY}}" 2>/dev/null || true)
if [ "${sa_needs_api}" = "true" ]; then
continue
fi
fi
fi

# Try strategic merge patch; may fail for some pod types (e.g. static pods / mirror pods)
echo "Patching Pod ${ns}/${pod} to set spec.automountServiceAccountToken=false (may trigger restart via controller)"
set +e
kubectl patch pod "${pod}" -n "${ns}" --type='merge' -p '{
"spec": { "automountServiceAccountToken": false }
}' >/dev/null 2>&1
rc=$?
set -e
if [ $rc -ne 0 ]; then
echo " Warning: could not patch pod ${ns}/${pod}; it may be controlled by a higher-level workload. Patch the controller object instead."
fi
done

echo "=== Step 4: Verification ==="

pods_with_token_mount_after=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.automountServiceAccountToken != false)] | length')

echo "Pods with automountServiceAccountToken not explicitly set to false AFTER remediation: ${pods_with_token_mount_after}"

if [ "${pods_with_token_mount_after}" -gt 0 ]; then
echo "Some pods still have automountServiceAccountToken not set to false."
echo "These are likely in namespaces or workloads explicitly marked as requiring API access,"
echo "or managed by controllers that must be updated via their manifests/Helm/IaC."
fi

echo "Automation complete."