Skip to main content

Service Account Tokens Are Only Mounted Where Necessary

More Info:​

Pods that automount service account tokens without needing them expand the attack surface. Token mounting should be disabled where API access is not required.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS GKE

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify pods and service accounts that do not need API access

    • On any machine with kubectl access:
      kubectl get pods --all-namespaces -o wide
      kubectl get sa --all-namespaces
    • Based on application knowledge, list pods and service accounts that never call the Kubernetes API.
  2. Disable token automount on service accounts that do not need API access

    • On any machine with kubectl access, for each such service account:
      kubectl patch sa SERVICEACCOUNT_NAME \
      -n NAMESPACE \
      --type merge \
      -p '{"automountServiceAccountToken": false}'
    • For GitOps or manifest-based management, edit the ServiceAccount manifest and add:
      automountServiceAccountToken: false
      then apply:
      kubectl apply -f serviceaccount.yaml
  3. Disable token automount on pods that do not need API access (per‑pod override)

    • For standalone pods:
      kubectl patch pod POD_NAME \
      -n NAMESPACE \
      --type merge \
      -p '{"spec":{"automountServiceAccountToken":false}}'
      (This affects the existing pod; ensure your controllers or manifests won’t immediately recreate it without the setting.)
  4. Disable token automount on controllers (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs)

    • On any machine with kubectl access, for each controller whose workloads do not require API access:
      kubectl get deployment DEPLOYMENT_NAME -n NAMESPACE -o yaml > deployment.yaml
    • Edit deployment.yaml and under spec.template.spec add:
      automountServiceAccountToken: false
    • Apply the change:
      kubectl apply -f deployment.yaml
    • Repeat similarly for other controller kinds:
      kubectl get statefulset STATEFULSET_NAME -n NAMESPACE -o yaml > ss.yaml
      kubectl get daemonset DAEMONSET_NAME -n NAMESPACE -o yaml > ds.yaml
      kubectl get job JOB_NAME -n NAMESPACE -o yaml > job.yaml
      kubectl get cronjob CRONJOB_NAME -n NAMESPACE -o yaml > cj.yaml
      Edit each file under spec.template.spec and add automountServiceAccountToken: false, then:
      kubectl apply -f ss.yaml
      kubectl apply -f ds.yaml
      kubectl apply -f job.yaml
      kubectl apply -f cj.yaml
  5. Review for exceptions where API access is required

    • Confirm you did not change service accounts or pods that legitimately need to call the Kubernetes API (e.g., controllers, operators, metrics collectors).
    • For critical namespaces (like kube-system), review individually before applying changes:
      kubectl get pods -n kube-system -o wide
      kubectl get sa -n kube-system -o yaml
  6. Verification

    • On any machine with kubectl access, re-run a refined audit to list only pods still mounting tokens:
      kubectl get pods --all-namespaces -o json | jq '
      .items[]
      | select(.spec.automountServiceAccountToken != false)
      | {namespace: .metadata.namespace, name: .metadata.name, automountServiceAccountToken: .spec.automountServiceAccountToken}'
    • Confirm that pods and service accounts which do not need API access no longer appear with automountServiceAccountToken set (either missing or true).
Using kubectl
# 1) Identify pods that explicitly mount a service account token
# Run on: any machine with kubectl access
kubectl get pods --all-namespaces -o jsonpath='{range .items[?(@.spec.automountServiceAccountToken!=false)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}'

# 2) Identify service accounts that explicitly allow token automount
kubectl get serviceaccounts --all-namespaces -o jsonpath='{range .items[?(@.automountServiceAccountToken!=false)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}'

Update service accounts (preferred, so all attached pods default to no token unless overridden):

# Example: disable token automount for a specific service account
# Replace NAMESPACE and SERVICEACCOUNT with real values
kubectl -n NAMESPACE patch serviceaccount SERVICEACCOUNT \
--type merge \
-p '{"automountServiceAccountToken": false}'

Update pod specs via their owning workload (Deployment/DaemonSet/StatefulSet/CronJob/Job):

# Example: patch a Deployment to disable token automount for its pods
# Replace NAMESPACE and DEPLOYMENT with real values
kubectl -n NAMESPACE patch deployment DEPLOYMENT \
--type merge \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

Declarative manifest snippets (for future changes):

# ServiceAccount manifest
apiVersion: v1
kind: ServiceAccount
metadata:
name: example-sa
namespace: example-namespace
automountServiceAccountToken: false
# Pod template in a Deployment (same pattern for other controllers)
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deployment
namespace: example-namespace
spec:
replicas: 1
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
serviceAccountName: example-sa
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27

Apply manifests:

# Run on: any machine with kubectl access
kubectl apply -f /absolute/path/to/your-manifest.yaml

Verification (adapted from the audit):

# 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"
else
echo "No pods with automountServiceAccountToken enabled (all are false or unset via SA defaults)."
fi
Automation
#!/usr/bin/env bash
#
# Purpose:
# Disable automountServiceAccountToken where it is not explicitly needed by
# setting:
# - Pod spec.automountServiceAccountToken: false
# - ServiceAccount automountServiceAccountToken: false
#
# Scope:
# - Runs from any machine with kubectl access and sufficient RBAC.
# - Idempotent: safe to re-run.
#
# WARNING:
# This is a conservative baseline hardening script. It will:
# * Set automountServiceAccountToken: false on ALL ServiceAccounts
# that do not already have it explicitly set to true.
# * Set automountServiceAccountToken: false on ALL Pods (workloads)
# that do not already have it explicitly set to true.
# Review before use and consider scoping to selected namespaces/labels
# if some workloads need default token mounting.

set -euo pipefail

#------------ Configuration ------------#

KUBECTL_BIN="${KUBECTL_BIN:-kubectl}"

# Optional: limit to specific namespaces, space-separated (leave empty for all)
NAMESPACE_FILTER="${NAMESPACE_FILTER:-}"

#------------ Helper Functions ------------#

require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: required command '$1' not found in PATH" >&2
exit 1
fi
}

run_kubectl() {
# Wrapper for kubectl to centralize binary selection
"$KUBECTL_BIN" "$@"
}

#------------ Pre-flight Checks ------------#

require_cmd "$KUBECTL_BIN"
require_cmd jq

echo "Using kubectl: $KUBECTL_BIN"
echo "Namespace filter: ${NAMESPACE_FILTER:-<all namespaces>}"

# Determine namespaces to operate on
if [ -n "$NAMESPACE_FILTER" ]; then
read -r -a NAMESPACES <<<"$NAMESPACE_FILTER"
else
mapfile -t NAMESPACES < <(run_kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
fi

echo "Target namespaces:"
printf ' %s\n' "${NAMESPACES[@]}"

#------------ Function: Patch ServiceAccounts ------------#

harden_serviceaccounts() {
local ns="$1"
echo "Processing ServiceAccounts in namespace: $ns"

# Get all SAs as JSON
local sa_json
if ! sa_json=$(run_kubectl get sa -n "$ns" -o json 2>/dev/null); then
echo " Skipping namespace $ns (unable to list ServiceAccounts)"
return
fi

# For each SA where .automountServiceAccountToken is null or not set or false,
# we will explicitly set it to false (if not already true).
# This preserves any explicit true values.
echo "$sa_json" \
| jq -r '
.items[]
| select(.automountServiceAccountToken != true)
| .metadata.name
' \
| while read -r sa_name; do
[ -z "$sa_name" ] && continue
echo " Patching ServiceAccount/$sa_name: set automountServiceAccountToken=false"
run_kubectl patch sa "$sa_name" -n "$ns" \
--type merge \
-p '{"automountServiceAccountToken": false}' >/dev/null
done
}

#------------ Function: Patch Workloads (Pods via controllers) ------------#

# This section adjusts controllers, not live Pods directly:
# - Deployments
# - StatefulSets
# - DaemonSets
# - ReplicaSets
# - Jobs
# - CronJobs

harden_workloads() {
local ns="$1"
echo "Processing workloads in namespace: $ns"

# Helper to patch a controller kind
local kind
for kind in deployment statefulset daemonset replicaset job cronjob; do
# Skip kinds not present in this namespace
if ! run_kubectl get "$kind" -n "$ns" >/dev/null 2>&1; then
continue
fi

local items_json
if ! items_json=$(run_kubectl get "$kind" -n "$ns" -o json 2>/dev/null); then
echo " Unable to list $kind in $ns; skipping"
continue
fi

# For each object whose pod template does NOT explicitly set
# spec.template.spec.automountServiceAccountToken to true,
# we set it to false.
echo "$items_json" \
| jq -r '
.items[]
| select(.spec.template.spec.automountServiceAccountToken != true)
| .metadata.name
' \
| while read -r name; do
[ -z "$name" ] && continue
echo " Patching ${kind}/${name}: set spec.template.spec.automountServiceAccountToken=false"
run_kubectl patch "$kind" "$name" -n "$ns" \
--type merge \
-p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}' >/dev/null
done
done
}

#------------ Main: Apply Fixes ------------#

for ns in "${NAMESPACES[@]}"; do
harden_serviceaccounts "$ns"
harden_workloads "$ns"
done

#------------ Verification ------------#

echo
echo "Verification: listing pods with automountServiceAccountToken not explicitly false."
echo "This should ideally return only pods that explicitly require token mounting."

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

echo "Pods with automountServiceAccountToken enabled or unset: $pods_with_token_mount"

if [ "$pods_with_token_mount" -eq 0 ]; then
echo "SUCCESS: All pods now have automountServiceAccountToken explicitly set to false."
else
echo "NOTICE: Some pods still have automountServiceAccountToken enabled or unset."
echo " Review them manually to confirm they genuinely need API access."
fi