Skip to main content

Service Account Tokens Are Only Mounted Where Necessary

More Info:

Service accounts tokens should not be mounted in pods except where the workload running in the pod explicitly needs to communicate with the API server

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. Identify pods and service accounts that are not compliant

    • 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 non-compliant pods and decide which workloads actually need API server access. Only those should keep automountServiceAccountToken: true or notset as appropriate.
  2. Disable token automount at the ServiceAccount level where API access is not needed

    • For each namespace and service account that should not get tokens by default, run on any machine with kubectl access:
      kubectl -n NAMESPACE get serviceaccount SERVICEACCOUNT -o yaml > /tmp/serviceaccount-NAMESPACE-SERVICEACCOUNT.yaml
    • Edit /tmp/serviceaccount-NAMESPACE-SERVICEACCOUNT.yaml and ensure this field is present under metadata (top level of the ServiceAccount):
      automountServiceAccountToken: false
    • Apply the change:
      kubectl apply -f /tmp/serviceaccount-NAMESPACE-SERVICEACCOUNT.yaml
  3. Override specific Pods to allow token mount when needed

    • For workloads that truly require API access and use a ServiceAccount you have set to automountServiceAccountToken: false, explicitly set pod-level automountServiceAccountToken: true so they keep a token:
      kubectl -n NAMESPACE get deployment DEPLOYMENT_NAME -o yaml > /tmp/deployment-NAMESPACE-DEPLOYMENT_NAME.yaml
    • Edit /tmp/deployment-NAMESPACE-DEPLOYMENT_NAME.yaml and, under spec.template.spec, add or set:
      automountServiceAccountToken: true
    • Apply the change:
      kubectl apply -f /tmp/deployment-NAMESPACE-DEPLOYMENT_NAME.yaml
    • This will trigger a rollout; existing pods in that workload will be recreated.
  4. Disable token automount at the Pod level for workloads that do not need API access

    • For each non-compliant workload you determined does not need API access, edit the controller manifest (Deployment/StatefulSet/DaemonSet/CronJob, etc.):
      kubectl -n NAMESPACE get deployment DEPLOYMENT_NAME -o yaml > /tmp/deployment-NAMESPACE-DEPLOYMENT_NAME.yaml
    • Under spec.template.spec, add or set:
      automountServiceAccountToken: false
    • Apply the manifest:
      kubectl apply -f /tmp/deployment-NAMESPACE-DEPLOYMENT_NAME.yaml
    • This will recreate pods for that workload without mounting service account tokens.
  5. For bare Pods (not managed by a controller), recreate them with the correct setting

    • Export and edit the Pod definition:
      kubectl -n NAMESPACE get pod POD_NAME -o yaml --export=false > /tmp/pod-NAMESPACE-POD_NAME.yaml
    • Remove metadata.resourceVersion, metadata.uid, metadata.creationTimestamp, status, and other server-generated fields from the file.
    • Under spec, set:
      automountServiceAccountToken: false
    • Delete and recreate the Pod:
      kubectl -n NAMESPACE delete pod POD_NAME
      kubectl apply -f /tmp/pod-NAMESPACE-POD_NAME.yaml
  6. Verification

    • After changes have rolled out and pods are Running, re-run the audit 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 pods compliant with 5.1.6"
Using kubectl

On any machine with kubectl access:

  1. Identify non-compliant Pods and ServiceAccounts (for review)
kubectl get pods --all-namespaces -o wide
kubectl get serviceaccounts --all-namespaces
  1. Patch a ServiceAccount so that tokens are not mounted by default
kubectl patch serviceaccount default \
-n your-namespace \
--type merge \
-p '{"automountServiceAccountToken": false}'

Replace default and your-namespace with the specific ServiceAccount and namespace that do not need API access.

Declarative example (ServiceAccount manifest):

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

Apply:

kubectl apply -f serviceaccount-example-sa.yaml
  1. Override at Pod/Workload level where the Pod must NOT mount the token

For individual Pods:

apiVersion: v1
kind: Pod
metadata:
name: example-pod
namespace: your-namespace
spec:
serviceAccountName: example-sa
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27
kubectl apply -f pod-example-pod.yaml

For Deployments (or similar controllers):

apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
namespace: your-namespace
spec:
replicas: 2
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
serviceAccountName: example-sa
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27
kubectl apply -f deployment-example.yaml
  1. Override at Pod/Workload level where the Pod MUST mount the token (explicitly true)

If you have set automountServiceAccountToken: false on a ServiceAccount but a particular Pod using it needs API access, set it to true in the Pod spec (Pod takes precedence):

spec:
serviceAccountName: example-sa
automountServiceAccountToken: true
containers:
- name: app
image: your-image

Apply with kubectl apply -f ....

  1. Verification (on any machine with kubectl access)

Re-run the benchmark-style audit and review that is_compliant is true for Pods that should not mount the token:

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
Automation
#!/usr/bin/env bash
#
# Automate CIS Kubernetes 5.1.6 remediation:
# Ensure Service Account Tokens Are Only Mounted Where Necessary
#
# Requirements:
# - Run on any machine with kubectl access and jq installed.
# - Assumes cluster-admin (or equivalent) privileges.
#
# Behavior:
# - For each Pod using a real ServiceAccount (not "default"):
# - If Pod.spec.automountServiceAccountToken is "true" or not set,
# patch the ServiceAccount to automountServiceAccountToken=false
# (if not already false).
# - If Pod.spec.automountServiceAccountToken is "true",
# patch the Pod to automountServiceAccountToken=false.
# - Skips "kube-system" namespace and DaemonSets/StatefulSets/Deployments,
# because those should be fixed at controller/manifest level.
# - Safe to re-run (uses server-side apply / merge patches).
# - Finishes by re-running the benchmark audit logic to show compliance.

set -euo pipefail

# CONFIGURABLE: namespaces to exclude from automatic changes
EXCLUDED_NAMESPACES_REGEX='^(kube-system|kube-public|kube-node-lease)$'

echo "Discovering pods and service accounts..."
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
# Skip excluded namespaces
if [[ "${pod_namespace}" =~ ${EXCLUDED_NAMESPACES_REGEX} ]]; then
continue
fi

# Skip pods without a service account or with default SA if you prefer to handle defaults manually
if [ -z "${pod_service_account}" ] || [ "${pod_service_account}" = "<none>" ]; then
continue
fi

# Normalize pod automount value
pod_is_automountserviceaccounttoken=$(echo "${pod_is_automountserviceaccounttoken}" | sed -e 's/<none>/notset/g' -e 's/null/notset/g')

# Fetch current SA automount value (may be null)
svacc_is_automountserviceaccounttoken=$(
kubectl get serviceaccount -n "${pod_namespace}" "${pod_service_account}" -o json \
| jq -r '.automountServiceAccountToken // "notset"' \
| sed -e 's/<none>/notset/g' -e 's/null/notset/g'
)

# Determine owning controller to avoid patching pods that will be recreated immediately
owner_kind=$(kubectl get pod "${pod_name}" -n "${pod_namespace}" -o jsonpath='{.metadata.ownerReferences[0].kind}' 2>/dev/null || echo "")
if [ -n "${owner_kind}" ] && [[ "${owner_kind}" =~ ^(DaemonSet|StatefulSet|ReplicaSet|Deployment|Job|CronJob)$ ]]; then
# These should be fixed by editing the controller manifests instead
continue
fi

echo "------------------------------------------------------------"
echo "Namespace: ${pod_namespace}"
echo "Pod: ${pod_name}"
echo "ServiceAccount: ${pod_service_account}"
echo "Pod automountServiceAccountToken: ${pod_is_automountserviceaccounttoken}"
echo "SA automountServiceAccountToken: ${svacc_is_automountserviceaccounttoken}"

# Decide if remediation is needed by reproducing compliance logic

is_compliant="false"
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"
fi

if [ "${is_compliant}" = "true" ]; then
echo "Status: already compliant, skipping."
continue
fi

echo "Status: NON-COMPLIANT, applying remediation..."

# 1) Ensure ServiceAccount has automountServiceAccountToken=false
if [ "${svacc_is_automountserviceaccounttoken}" != "false" ]; then
echo " - Patching ServiceAccount ${pod_service_account} in ${pod_namespace} to automountServiceAccountToken=false"
kubectl patch serviceaccount "${pod_service_account}" \
-n "${pod_namespace}" \
--type='merge' \
-p '{"automountServiceAccountToken": false}'
else
echo " - ServiceAccount already has automountServiceAccountToken=false"
fi

# 2) If Pod spec explicitly sets automountServiceAccountToken to true, patch it to false
if [ "${pod_is_automountserviceaccounttoken}" = "true" ]; then
echo " - Patching Pod ${pod_name} in ${pod_namespace} to automountServiceAccountToken=false"
kubectl patch pod "${pod_name}" \
-n "${pod_namespace}" \
--type='merge' \
-p '{"spec":{"automountServiceAccountToken": false}}'
elif [ "${pod_is_automountserviceaccounttoken}" = "notset" ]; then
# With SA set to false, Pod not setting automount is compliant; no need to patch Pod.
echo " - Pod does not set automountServiceAccountToken; SA=false is sufficient"
else
echo " - Pod already has automountServiceAccountToken=false"
fi

done

echo
echo "Verification: re-running compliance evaluation..."
echo

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 2>/dev/null \
| jq -r '.automountServiceAccountToken // "notset"' \
| 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

echo
echo "Automation complete. Review any remaining non-compliant workloads (likely controller-managed) and update their manifests accordingly."

Additional Reading: