Skip to main content

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

Manual Steps
  1. 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_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 output and decide which workloads truly need API server access. Only those should continue to mount tokens.
  2. 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.
  3. 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.
  4. 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: true if 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: false as in Step 2 so the Pod-level value overrides the ServiceAccount.
  5. 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: v1
        kind: ServiceAccount
        metadata:
        name: SERVICEACCOUNT_NAME
        namespace: NAMESPACE
        automountServiceAccountToken: false
      • In every Pod template that must explicitly disable mounting:
        spec:
        serviceAccountName: SERVICEACCOUNT_NAME
        automountServiceAccountToken: false
    • Apply the updated manifests:
    kubectl apply -f PATH/TO/MANIFEST.yaml
  6. 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_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 evaluated Pods are compliant"
Using kubectl

On any machine with kubectl access:

  1. 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.

  1. 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}'
  1. For pods created by controllers (Deployments, DaemonSets, StatefulSets, Jobs, CronJobs), set automountServiceAccountToken: false in 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.

  1. 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
  1. 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