Service Account Tokens Are Only Mounted Where Necessary
More Info:​
Pods and service accounts that do not call the API server should not mount service account tokens, reducing the credentials exposed in the workload.
Risk Level​
Medium
Address​
Security
Compliance Standards​
- CIS Kubernetes
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify non-compliant Pods and their ServiceAccounts
- Run on: any machine with kubectl access
kubectl get pods --all-namespaces -o custom-columns=POD_NAMESPACE:.metadata.namespace,POD_NAME:.metadata.name,POD_SERVICE_ACCOUNT:.spec.serviceAccount,POD_IS_AUTOMOUNTSERVICEACCOUNTTOKEN:.spec.automountServiceAccountToken --no-headers | while read -r pod_namespace pod_name pod_service_account pod_is_automountserviceaccounttokendosvacc_is_automountserviceaccounttoken=$(kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json | jq -r '.automountServiceAccountToken' | sed -e 's/<none>/notset/g' -e 's/null/notset/g')pod_is_automountserviceaccounttoken=$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')if [ "${svacc_is_automountserviceaccounttoken}" = "false" ] && ( [ "${pod_is_automountserviceaccounttoken}" = "false" ] || [ "${pod_is_automountserviceaccounttoken}" = "notset" ] ); thenis_compliant="true"elif [ "${svacc_is_automountserviceaccounttoken}" = "true" ] && [ "${pod_is_automountserviceaccounttoken}" = "false" ]; thenis_compliant="true"elseis_compliant="false"fiecho "**namespace: ${pod_namespace} pod_name: ${pod_name} service_account: ${pod_service_account} pod_is_automountserviceaccounttoken: ${pod_is_automountserviceaccounttoken} svacc_is_automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken} is_compliant: ${is_compliant}"done | grep 'is_compliant: false'- Review the output and decide which workloads truly need API server access. Only those should continue to mount tokens.
-
For Pods that do NOT need API access but MUST keep using their current ServiceAccount
- Run on: any machine with kubectl access
- For each such Pod, edit the controller that creates it (Deployment/StatefulSet/DaemonSet/CronJob/Job) and set
spec.template.spec.automountServiceAccountToken: false. Example for a Deployment:
kubectl -n NAMESPACE edit deployment DEPLOYMENT_NAME- Add or update under
spec.template.spec:
spec:template:spec:automountServiceAccountToken: false- Save and exit; the Pods will be recreated automatically without mounting tokens.
-
For ServiceAccounts used ONLY by Pods that do NOT need API access
- Run on: any machine with kubectl access
- Edit the ServiceAccount to disable token automounting by default:
kubectl -n NAMESPACE edit serviceaccount SERVICEACCOUNT_NAME- Add or update at top-level of the ServiceAccount spec:
automountServiceAccountToken: false- This makes all Pods using this ServiceAccount compliant as long as their Pod spec does not override with
true.
-
For ServiceAccounts shared by Pods that DO and DO NOT need API access
- Run on: any machine with kubectl access
- Keep the ServiceAccount as-is (or even
automountServiceAccountToken: trueif needed), and control per-Pod behavior:- For Pods that need API access: no change required (they may inherit the default).
- For Pods that must NOT mount tokens: set
spec.template.spec.automountServiceAccountToken: falseas in Step 2 so the Pod-level value overrides the ServiceAccount.
-
For static workload manifests managed via files (Git/IaC) rather than
kubectl edit- Run on: any machine with kubectl access and repo access
- Locate the YAML manifest for each non-compliant workload and modify it directly:
- In every ServiceAccount that should not mount tokens by default:
apiVersion: v1kind: ServiceAccountmetadata:name: SERVICEACCOUNT_NAMEnamespace: NAMESPACEautomountServiceAccountToken: false
- In every Pod template that must explicitly disable mounting:
spec:serviceAccountName: SERVICEACCOUNT_NAMEautomountServiceAccountToken: false
- In every ServiceAccount that should not mount tokens by default:
- Apply the updated manifests:
kubectl apply -f PATH/TO/MANIFEST.yaml -
Verify remediation
- Run on: any machine with kubectl access
kubectl get pods --all-namespaces -o custom-columns=POD_NAMESPACE:.metadata.namespace,POD_NAME:.metadata.name,POD_SERVICE_ACCOUNT:.spec.serviceAccount,POD_IS_AUTOMOUNTSERVICEACCOUNTTOKEN:.spec.automountServiceAccountToken --no-headers | while read -r pod_namespace pod_name pod_service_account pod_is_automountserviceaccounttokendosvacc_is_automountserviceaccounttoken=$(kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json | jq -r '.automountServiceAccountToken' | sed -e 's/<none>/notset/g' -e 's/null/notset/g')pod_is_automountserviceaccounttoken=$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')if [ "${svacc_is_automountserviceaccounttoken}" = "false" ] && ( [ "${pod_is_automountserviceaccounttoken}" = "false" ] || [ "${pod_is_automountserviceaccounttoken}" = "notset" ] ); thenis_compliant="true"elif [ "${svacc_is_automountserviceaccounttoken}" = "true" ] && [ "${pod_is_automountserviceaccounttoken}" = "false" ]; thenis_compliant="true"elseis_compliant="false"fiecho "**namespace: ${pod_namespace} pod_name: ${pod_name} service_account: ${pod_service_account} pod_is_automountserviceaccounttoken: ${pod_is_automountserviceaccounttoken} svacc_is_automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken} is_compliant: ${is_compliant}"done | grep 'is_compliant: false' || echo "All evaluated Pods are compliant"
Using kubectl
On any machine with kubectl access:
- Identify pods and service accounts to change (those that do not need API access but are non‑compliant):
kubectl get pods --all-namespaces -o custom-columns=POD_NAMESPACE:.metadata.namespace,POD_NAME:.metadata.name,POD_SERVICE_ACCOUNT:.spec.serviceAccount,POD_IS_AUTOMOUNTSERVICEACCOUNTTOKEN:.spec.automountServiceAccountToken --no-headers | while read -r pod_namespace pod_name pod_service_account pod_is_automountserviceaccounttoken
do
svacc_is_automountserviceaccounttoken=$(kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json | jq -r '.automountServiceAccountToken' | sed -e 's/<none>/notset/g' -e 's/null/notset/g')
pod_is_automountserviceaccounttoken=$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')
if [ "${svacc_is_automountserviceaccounttoken}" = "false" ] && ( [ "${pod_is_automountserviceaccounttoken}" = "false" ] || [ "${pod_is_automountserviceaccounttoken}" = "notset" ] ); then
is_compliant="true"
elif [ "${svacc_is_automountserviceaccounttoken}" = "true" ] && [ "${pod_is_automountserviceaccounttoken}" = "false" ]; then
is_compliant="true"
else
is_compliant="false"
fi
echo "**namespace: ${pod_namespace} pod_name: ${pod_name} service_account: ${pod_service_account} pod_is_automountserviceaccounttoken: ${pod_is_automountserviceaccounttoken} svacc_is_automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken} is_compliant: ${is_compliant}"
done | grep "is_compliant: false"
Review the listed workloads and confirm they do not require Kubernetes API access before changing anything.
- Patch ServiceAccounts that should never auto‑mount tokens (declarative via kubectl patch):
kubectl patch serviceaccount my-serviceaccount \
-n my-namespace \
--type merge \
-p '{"automountServiceAccountToken": false}'
- For pods created by controllers (Deployments, DaemonSets, StatefulSets, Jobs, CronJobs), set
automountServiceAccountToken: falsein the pod template. Example for a Deployment:
kubectl patch deployment my-deployment \
-n my-namespace \
--type merge \
-p '{
"spec": {
"template": {
"spec": {
"automountServiceAccountToken": false
}
}
}
}'
Example for a DaemonSet:
kubectl patch daemonset my-daemonset \
-n my-namespace \
--type merge \
-p '{
"spec": {
"template": {
"spec": {
"automountServiceAccountToken": false
}
}
}
}'
Example for a StatefulSet:
kubectl patch statefulset my-statefulset \
-n my-namespace \
--type merge \
-p '{
"spec": {
"template": {
"spec": {
"automountServiceAccountToken": false
}
}
}
}'
Example for a Job:
kubectl patch job my-job \
-n my-namespace \
--type merge \
-p '{
"spec": {
"template": {
"spec": {
"automountServiceAccountToken": false
}
}
}
}'
Example for a CronJob:
kubectl patch cronjob my-cronjob \
-n my-namespace \
--type merge \
-p '{
"spec": {
"jobTemplate": {
"spec": {
"template": {
"spec": {
"automountServiceAccountToken": false
}
}
}
}
}
}'
These patches will roll out new pods with tokens not mounted.
- For standalone Pods defined directly (no controller), edit the pod manifest and re‑create it, or patch if acceptable (note: patching an existing running Pod will not change already mounted volumes). Recommended approach:
- Export, edit, and re‑create:
kubectl get pod my-pod -n my-namespace -o yaml > /tmp/my-pod.yaml
Edit /tmp/my-pod.yaml so the pod spec contains:
spec:
automountServiceAccountToken: false
Then delete and recreate:
kubectl delete pod my-pod -n my-namespace
kubectl apply -f /tmp/my-pod.yaml
- Verification (same logic as audit):
kubectl get pods --all-namespaces -o custom-columns=POD_NAMESPACE:.metadata.namespace,POD_NAME:.metadata.name,POD_SERVICE_ACCOUNT:.spec.serviceAccount,POD_IS_AUTOMOUNTSERVICEACCOUNTTOKEN:.spec.automountServiceAccountToken --no-headers | while read -r pod_namespace pod_name pod_service_account pod_is_automountserviceaccounttoken
do
svacc_is_automountserviceaccounttoken=$(kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json | jq -r '.automountServiceAccountToken' | sed -e 's/<none>/notset/g' -e 's/null/notset/g')
pod_is_automountserviceaccounttoken=$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')
if [ "${svacc_is_automountserviceaccounttoken}" = "false" ] && ( [ "${pod_is_automountserviceaccounttoken}" = "false" ] || [ "${pod_is_automountserviceaccounttoken}" = "notset" ] ); then
is_compliant="true"
elif [ "${svacc_is_automountserviceaccounttoken}" = "true" ] && [ "${pod_is_automountserviceaccounttoken}" = "false" ]; then
is_compliant="true"
else
is_compliant="false"
fi
echo "**namespace: ${pod_namespace} pod_name: ${pod_name} service_account: ${pod_service_account} pod_is_automountserviceaccounttoken: ${pod_is_automountserviceaccounttoken} svacc_is_automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken} is_compliant: ${is_compliant}"
done | grep "is_compliant: false" || echo "All pods compliant with automountServiceAccountToken policy"
Automation
#!/usr/bin/env bash
# Automation for: Ensure Service Account Tokens Are Only Mounted Where Necessary (CIS 5.1.6)
# Runs on: any machine with kubectl access and jq installed.
# Behavior:
# - Scans all pods
# - For pods that do NOT need a token (user-provided list), ensures:
# * Pod spec has .spec.automountServiceAccountToken=false (by patching the controller)
# * The bound ServiceAccount has automountServiceAccountToken=false
# - Safe to re-run (idempotent)
set -euo pipefail
# -----------------------------
# CONFIGURATION (EDIT REQUIRED)
# -----------------------------
# You MUST define which workloads do NOT need API access.
# Format: "namespace:kind/name" where kind is one of Deployment,StatefulSet,DaemonSet,Job,CronJob
# Example:
# NO_TOKEN_WORKLOADS=(
# "default:Deployment/nginx"
# "batch:Job/offline-processor"
# )
NO_TOKEN_WORKLOADS=(
# "default:Deployment/nginx"
)
if [ "${#NO_TOKEN_WORKLOADS[@]}" -eq 0 ]; then
echo "No workloads specified in NO_TOKEN_WORKLOADS. Nothing to do."
exit 0
fi
# --------------
# PREREQUISITES
# --------------
command -v kubectl >/dev/null 2>&1 || { echo "kubectl not found in PATH"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found in PATH"; exit 1; }
# Ensure we can talk to the cluster
kubectl version --client >/dev/null 2>&1 || { echo "kubectl not configured correctly"; exit 1; }
# --------------
# HELPER FUNCS
# --------------
patch_controller() {
local ns="$1" kind="$2" name="$3"
# Determine the pod template path for automountServiceAccountToken
local path
case "$kind" in
Deployment|StatefulSet|DaemonSet)
path=".spec.template.spec"
;;
Job)
path=".spec.template.spec"
;;
CronJob)
path=".spec.jobTemplate.spec.template.spec"
;;
*)
echo "Unsupported controller kind: $kind in ${ns}/${name}, skipping" >&2
return
;;
esac
# Check current value
local current
current="$(kubectl get "$kind" "$name" -n "$ns" -o json | jq -r "${path}.automountServiceAccountToken // \"notset\"")"
if [ "$current" = "false" ]; then
echo "Controller ${kind}/${name} in namespace ${ns}: automountServiceAccountToken already false"
else
echo "Patching controller ${kind}/${name} in namespace ${ns} to set automountServiceAccountToken=false"
kubectl patch "$kind" "$name" -n "$ns" \
--type='merge' \
-p "{\"spec\":$(kubectl get \"$kind\" \"$name\" -n \"$ns\" -o json | \
jq -c --argjson val false \
"$(if [ "$kind" = "CronJob" ]; then
echo '.spec.jobTemplate.spec.template.spec.automountServiceAccountToken=$val'
else
echo '.spec.template.spec.automountServiceAccountToken=$val'
fi)")}"
fi
# Get serviceAccountName from the controller template
local sa_name
sa_name="$(kubectl get "$kind" "$name" -n "$ns" -o json | jq -r "${path}.serviceAccountName // \"default\"")"
# Ensure ServiceAccount has automountServiceAccountToken=false
if kubectl get serviceaccount "$sa_name" -n "$ns" >/dev/null 2>&1; then
local sa_current
sa_current="$(kubectl get serviceaccount "$sa_name" -n "$ns" -o json | jq -r '.automountServiceAccountToken // "notset"')"
if [ "$sa_current" = "false" ]; then
echo "ServiceAccount ${ns}/${sa_name}: automountServiceAccountToken already false"
else
echo "Patching ServiceAccount ${ns}/${sa_name} to set automountServiceAccountToken=false"
kubectl patch serviceaccount "$sa_name" -n "$ns" \
--type='merge' \
-p '{"automountServiceAccountToken":false}'
fi
else
echo "ServiceAccount ${ns}/${sa_name} not found (referenced by ${kind}/${name}), skipping SA patch" >&2
fi
}
# --------------
# MAIN EXECUTION
# --------------
echo "Applying automountServiceAccountToken=false to configured workloads and their ServiceAccounts..."
for entry in "${NO_TOKEN_WORKLOADS[@]}"; do
ns="${entry%%:*}"
rest="${entry#*:}"
kind="${rest%%/*}"
name="${rest#*/}"
if [ -z "$ns" ] || [ -z "$kind" ] || [ -z "$name" ]; then
echo "Invalid entry in NO_TOKEN_WORKLOADS: ${entry} (expected namespace:Kind/name)" >&2
continue
fi
if ! kubectl get "$kind" "$name" -n "$ns" >/dev/null 2>&1; then
echo "Controller ${kind}/${name} not found in namespace ${ns}, skipping" >&2
continue
fi
patch_controller "$ns" "$kind" "$name"
done
# --------------
# VERIFICATION
# --------------
echo
echo "Verification: checking pods and service accounts for non-compliant automountServiceAccountToken settings..."
kubectl get pods --all-namespaces -o custom-columns=POD_NAMESPACE:.metadata.namespace,POD_NAME:.metadata.name,POD_SERVICE_ACCOUNT:.spec.serviceAccount,POD_IS_AUTOMOUNTSERVICEACCOUNTTOKEN:.spec.automountServiceAccountToken --no-headers | \
while read -r pod_namespace pod_name pod_service_account pod_is_automountserviceaccounttoken; do
# Normalize pod SA name (empty means 'default')
if [ -z "$pod_service_account" ] || [ "$pod_service_account" = "<none>" ] || [ "$pod_service_account" = "null" ]; then
pod_service_account="default"
fi
svacc_is_automountserviceaccounttoken="$(kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json 2>/dev/null | jq -r '.automountServiceAccountToken' | sed -e 's/<none>/notset/g' -e 's/null/notset/g')"
pod_is_automountserviceaccounttoken="$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')"
if [ "${svacc_is_automountserviceaccounttoken}" = "false" ] && { [ "${pod_is_automountserviceaccounttoken}" = "false" ] || [ "${pod_is_automountserviceaccounttoken}" = "notset" ]; }; then
is_compliant="true"
elif { [ "${svacc_is_automountserviceaccounttoken}" = "true" ] || [ "${svacc_is_automountserviceaccounttoken}" = "notset" ]; } && [ "${pod_is_automountserviceaccounttoken}" = "false" ]; then
is_compliant="true"
else
is_compliant="false"
fi
echo "namespace: ${pod_namespace} pod_name: ${pod_name} service_account: ${pod_service_account} pod_is_automountserviceaccounttoken: ${pod_is_automountserviceaccounttoken} svacc_is_automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken} is_compliant: ${is_compliant}"
done