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โ€‹

  • APRA CPS 234 (Australia)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS GKE
  • CMMC 2.0
  • CSA Cloud Controls Matrix v4
  • DPDPA
  • Digital Operational Resilience Act (EU)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIS2 Directive
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • UK NCSC Cyber Assessment Framework

Triage and Remediationโ€‹

Remediationโ€‹

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

    kubectl get pods --all-namespaces -o custom-columns='NAMESPACE:.metadata.namespace,POD:.metadata.name,SA:.spec.serviceAccountName,AMOUNT:.spec.automountServiceAccountToken'

    Review each workloadโ€™s function; mark those that do NOT need to call the Kubernetes API.

  2. For pods that do NOT need API access and are defined directly as Pods (no higher-level controller), edit them to disable token mounting (run on any machine with kubectl access):

    kubectl edit pod <pod-name> -n <namespace>

    In the spec: section of the pod, add or set:

    spec:
    automountServiceAccountToken: false

    Save and exit; the Pod will be updated or recreated depending on how itโ€™s managed.

  3. For deployments/daemonsets/statefulsets/jobs/cronjobs whose pods do NOT need API access, edit the controller spec so all future pods have token mounting disabled (run on any machine with kubectl access). Example for a Deployment:

    kubectl edit deployment <deployment-name> -n <namespace>

    Under spec.template.spec, add or set:

    spec:
    template:
    spec:
    automountServiceAccountToken: false

    Save and exit; this will trigger a rollout with updated pod specs.

  4. For service accounts where most or all attached pods do NOT need tokens, set the service account default to not mount tokens, then override only for exceptional pods (run on any machine with kubectl access):

    kubectl edit serviceaccount <sa-name> -n <namespace>

    Add or set:

    automountServiceAccountToken: false

    Then, for the few pods that DO need API access and use this service account, explicitly enable mounting in their pod template:

    spec:
    serviceAccountName: <sa-name>
    automountServiceAccountToken: true
  5. For Git-managed manifests, make the same changes in the YAML definitions instead of using kubectl edit, then apply them (run on any machine with kubectl access):

    kubectl apply -f <updated-manifest>.yaml

    Ensure:

    • Pod specs that do NOT need API access have spec.automountServiceAccountToken: false.
    • Service accounts that should not auto-mount tokens have automountServiceAccountToken: false.
  6. Verify that only pods which truly need tokens are mounting them (run on any machine with kubectl access):

    echo "๐Ÿ”น Pods with automountServiceAccountToken enabled:"
    pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
    [.items[] | select(.spec.automountServiceAccountToken != false)] | length')

    if [ "$pods_with_token_mount" -gt 0 ]; then
    echo "automountServiceAccountToken"
    kubectl get pods --all-namespaces -o json | jq -r '
    .items[]
    | select(.spec.automountServiceAccountToken != false)
    | [.metadata.namespace, .metadata.name, (.spec.serviceAccountName // "default")]
    | @tsv'
    else
    echo "No pods with automountServiceAccountToken enabled by default (all are false or unset by design)."
    fi

    Manually confirm that any listed pods are ones that intentionally require API server access.

Using kubectl

On any machine with kubectl access:

  1. Identify pods and their service accounts
kubectl get pods --all-namespaces -o wide
kubectl get sa --all-namespaces -o wide
  1. For pods that do NOT need API access: disable token mount on the Pod spec
    (works for pods managed by higher-level controllers via their templates).

Example: for a Deployment (replace values as needed):

kubectl -n default get deploy my-deployment -o yaml > my-deployment.yaml

Edit my-deployment.yaml and in .spec.template.spec ensure:

spec:
automountServiceAccountToken: false
# existing containers/fields...

Apply the change:

kubectl apply -f my-deployment.yaml

Repeat similarly for other controllers (DaemonSet, StatefulSet, Job, CronJob) by editing spec.template.spec.automountServiceAccountToken: false.

For standalone Pods (not recommended in production), edit directly:

kubectl -n default get pod my-pod -o yaml > my-pod.yaml
# edit .spec.automountServiceAccountToken: false
kubectl delete pod -n default my-pod
kubectl apply -f my-pod.yaml
  1. For service accounts used by multiple workloads: disable token mount at SA level

Edit each service account whose workloads do NOT need API access:

kubectl -n default get sa my-serviceaccount -o yaml > my-serviceaccount.yaml

Edit my-serviceaccount.yaml:

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

Apply:

kubectl apply -f my-serviceaccount.yaml

Note: a Podโ€™s own spec.automountServiceAccountToken (if set) overrides the ServiceAccount setting.

  1. Verification

On any machine with kubectl access:

echo "๐Ÿ”น Pods with automountServiceAccountToken enabled (or defaulting to true):"
pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
[.items[] | select(.spec.automountServiceAccountToken != false)] | length')

if [ "$pods_with_token_mount" -gt 0 ]; then
echo "automountServiceAccountToken"
else
echo "No pods found with automountServiceAccountToken enabled/default-true based on Pod spec."
fi
Automation
#!/usr/bin/env bash
#
# Disable automountServiceAccountToken for pods and service accounts
# that are NOT in an allowlist and currently have it enabled/implicit.
#
# IMPORTANT: This is a generic enforcement script for environments where
# most workloads do NOT need API access. You MUST review and adapt the
# allowlist variables below to avoid breaking workloads that legitimately
# use the Kubernetes API.
#
# Run from: any machine with kubectl access and a current context.
# Requirements: bash, kubectl, jq

set -euo pipefail

# --------- CONFIGURABLE ALLOWLISTS (EDIT TO FIT YOUR CLUSTER) ---------

# Namespaces where we will NOT touch automountServiceAccountToken
# (typically system/control-plane or known platform namespaces)
NAMESPACE_ALLOWLIST=(
"kube-system"
"kube-public"
"kube-node-lease"
"gatekeeper-system"
"istio-system"
"linkerd"
)

# ServiceAccounts (namespace/name) that are allowed to keep tokens
# even if their namespace is not allowlisted.
SA_ALLOWLIST=(
# "default/my-api-client-sa"
)

# Label selector for pods that are allowed to keep tokens
# (empty string to ignore; otherwise something like "security.k8s.io/needs-api=true")
POD_ALLOWLIST_LABEL_SELECTOR=""

# ---------------------------------------------------------------------

command -v kubectl >/dev/null 2>&1 || {
echo "kubectl not found in PATH" >&2
exit 1
}

command -v jq >/dev/null 2>&1 || {
echo "jq not found in PATH" >&2
exit 1
}

# Convert bash arrays to jq-friendly filters
ns_allowlist_json=$(printf '%s\n' "${NAMESPACE_ALLOWLIST[@]:-}" | jq -R . | jq -s .)
sa_allowlist_json=$(printf '%s\n' "${SA_ALLOWLIST[@]:-}" | jq -R . | jq -s .)

is_ns_allowlisted() {
local ns="$1"
jq -e --arg ns "$ns" -n --argjson arr "$ns_allowlist_json" '
($arr | index($ns)) != null
' >/dev/null
}

is_sa_allowlisted() {
local ns="$1"
local name="$2"
local key="${ns}/${name}"
jq -e --arg key "$key" -n --argjson arr "$sa_allowlist_json" '
($arr | index($key)) != null
' >/dev/null
}

echo "=== Stage 1: Fix ServiceAccounts with automountServiceAccountToken not explicitly false ==="

# Get all SAs with automountServiceAccountToken unset or true
kubectl get serviceaccounts --all-namespaces -o json \
| jq -r '
.items[]
| select(.automountServiceAccountToken != false)
| [.metadata.namespace, .metadata.name]
| @tsv
' | while IFS=$'\t' read -r ns sa; do
# Skip allowlisted namespaces/SAs
if is_ns_allowlisted "$ns"; then
echo "Skipping SA $ns/$sa (namespace allowlisted)"
continue
fi
if is_sa_allowlisted "$ns" "$sa"; then
echo "Skipping SA $ns/$sa (SA allowlisted)"
continue
fi

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

echo "=== Stage 2: Fix Pods with automountServiceAccountToken not explicitly false ==="

# Optional label allowlist selector
pod_selector_args=()
if [ -n "$POD_ALLOWLIST_LABEL_SELECTOR" ]; then
# We will compute the allowed pods separately and skip them
:
fi

# Get all pods with automountServiceAccountToken unset or true
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| select(.spec.automountServiceAccountToken != false)
| [.metadata.namespace, .metadata.name, (.spec.serviceAccountName // "default")]
| @tsv
' | while IFS=$'\t' read -r ns pod sa; do
# Skip allowlisted namespaces
if is_ns_allowlisted "$ns"; then
echo "Skipping Pod $ns/$pod (namespace allowlisted)"
continue
fi

# Skip pods whose SA is allowlisted
if is_sa_allowlisted "$ns" "$sa"; then
echo "Skipping Pod $ns/$pod (SA $ns/$sa allowlisted)"
continue
fi

# Skip pods with allowlist label if configured
if [ -n "$POD_ALLOWLIST_LABEL_SELECTOR" ]; then
if kubectl get pod "$pod" -n "$ns" -l "$POD_ALLOWLIST_LABEL_SELECTOR" >/dev/null 2>&1; then
echo "Skipping Pod $ns/$pod (label allowlisted: $POD_ALLOWLIST_LABEL_SELECTOR)"
continue
fi
fi

echo "Patching Pod $ns/$pod to set spec.automountServiceAccountToken=false"
# This patch will fail for pods managed by controllers; it is still safe to try.
if ! kubectl patch pod "$pod" -n "$ns" \
--type merge \
-p '{"spec":{"automountServiceAccountToken": false}}' >/dev/null 2>&1; then
echo " Note: Could not patch Pod $ns/$pod (likely controlled by a workload object)."
echo " Please update the owning controller's manifest to disable automountServiceAccountToken."
fi
done

echo "=== Stage 3: Verification (matches benchmark audit intent) ==="

echo "๐Ÿ”น Pods with automountServiceAccountToken enabled (after remediation):"
pods_with_token_mount=$(kubectl get pods --all-namespaces -o json | jq '
[.items[]
| select(.spec.automountServiceAccountToken != false)
| select(.metadata.namespace as $ns
| (['"$ns_allowlist_json"'] | .[0] | index($ns)) == null)
] | length')

echo "Count (excluding allowlisted namespaces): $pods_with_token_mount"

if [ "$pods_with_token_mount" -gt 0 ]; then
echo "Some non-allowlisted pods still have automountServiceAccountToken enabled or implicit."
echo "Investigate controllers/manifests for those pods and decide case-by-case."
else
echo "All non-allowlisted pods explicitly have automountServiceAccountToken=false."
fi

echo "๐Ÿ”น ServiceAccounts with automountServiceAccountToken not explicitly false (after remediation):"
sa_with_token_mount=$(kubectl get serviceaccounts --all-namespaces -o json | jq '
[.items[]
| select(.automountServiceAccountToken != false)
| select(.metadata.namespace as $ns
| (['"$ns_allowlist_json"'] | .[0] | index($ns)) == null)
] | length')

echo "Count (excluding allowlisted namespaces): $sa_with_token_mount"

if [ "$sa_with_token_mount" -gt 0 ]; then
echo "Some non-allowlisted ServiceAccounts still have automountServiceAccountToken enabled or implicit."
echo "Review them and decide if they truly need API access."
else
echo "All non-allowlisted ServiceAccounts explicitly have automountServiceAccountToken=false."
fi

Additional Reading:โ€‹