Skip to main content

Pods That Do Not Use The API Should Disable Token Automount

More Info:​

Verifies automountServiceAccountToken is false for pods that do not call the Kubernetes API. A mounted token is a ready-made credential for an attacker who lands in the pod.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • Cloudanix Best Practice

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify noncompliant Pods and their owners (run on any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
    | (.spec.automountServiceAccountToken == false) as $ok
    | select($ok | not)
    | "ns=\($m.namespace) pod=\($m.name) ownerKind=\($own.kind // "Pod") ownerName=\($own.name // $m.name)"
    ][]'
  2. For each listed workload, decide if it legitimately calls the Kubernetes API (run on any machine with kubectl access):

    • Inspect container images, args, and env for in-cluster API use:
      kubectl -n NAMESPACE get DEPLOYMENT_NAME -o yaml
    • If the app needs to talk to the API (client libraries, KUBERNETES_SERVICE_HOST, in-cluster config, or curl to https://kubernetes.default.svc), do not disable the token; instead, review RBAC separately.
    • Only proceed to the next step for workloads that do not need API access.
  3. Disable token automount at the Pod spec level for controller-managed workloads (recommended) (run on any machine with kubectl access, adjust kind as appropriate: Deployment, DaemonSet, StatefulSet, Job, etc.):

    kubectl -n NAMESPACE patch deployment DEPLOYMENT_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'

    Example for a StatefulSet:

    kubectl -n NAMESPACE patch statefulset STATEFULSET_NAME \
    --type merge \
    -p '{"spec":{"template":{"spec":{"automountServiceAccountToken":false}}}}'
  4. For standalone Pods not managed by a controller, recreate them with token automount disabled (run on any machine with kubectl access):

    # Export current Pod spec
    kubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yaml

    # Edit the file locally
    sed -i 's/^spec:/spec:\n automountServiceAccountToken: false/' /tmp/pod-POD_NAME.yaml

    # Remove status and cluster-assigned fields
    yq -i 'del(.status,.metadata.uid,.metadata.resourceVersion,.metadata.selfLink,.metadata.creationTimestamp,.metadata.managedFields,.metadata.ownerReferences,.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration")' /tmp/pod-POD_NAME.yaml

    # Delete and recreate the Pod
    kubectl -n NAMESPACE delete pod POD_NAME
    kubectl -n NAMESPACE apply -f /tmp/pod-POD_NAME.yaml
  5. Optionally, disable token automount on the ServiceAccount used by multiple non-API workloads (use only if all pods using it do not need the API) (run on any machine with kubectl access):

    kubectl -n NAMESPACE patch serviceaccount SERVICEACCOUNT_NAME \
    --type merge \
    -p '{"automountServiceAccountToken":false}'
  6. Verify compliance (run on any machine with kubectl access):

    kubectl get pods --all-namespaces -o json | jq -r '
    [ .items[]
    | select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
    | .metadata as $m
    | (.spec.automountServiceAccountToken == false) as $ok
    | select($ok | not)
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

    Ensure the output is is_compliant=true or that no remaining noncompliant Pods correspond to workloads that can safely disable token automount.

Using kubectl

On any machine with kubectl access to the cluster:

  1. Identify a noncompliant pod and its owner (from the audit output), for example:

    • Namespace: prod
    • Pod name: web-abc123
    • Owner: Deployment/prod/web
  2. Export the owning workload manifest and edit it locally (example for a Deployment):

kubectl -n prod get deployment web -o yaml > /tmp/deployment-web.yaml
  1. In /tmp/deployment-web.yaml, under spec.template.spec, set automountServiceAccountToken: false. For example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
automountServiceAccountToken: false
containers:
- name: web
image: nginx:1.27
  1. Apply the updated manifest:
kubectl apply -f /tmp/deployment-web.yaml
  1. If the pod is created directly (no controller), patch it in place (note: this recreates the pod, not the spec from a controller):
kubectl -n prod patch pod web-abc123 \
--type='merge' \
-p '{"spec":{"automountServiceAccountToken":false}}'
  1. If you choose to set this at the ServiceAccount instead (applies to all pods using it):
kubectl -n prod get serviceaccount web-sa -o yaml > /tmp/sa-web-sa.yaml

Edit /tmp/sa-web-sa.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
name: web-sa
namespace: prod
automountServiceAccountToken: false

Apply:

kubectl apply -f /tmp/sa-web-sa.yaml
  1. Verification (rerun the audit on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.automountServiceAccountToken == false) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " serviceAccount=\(.spec.serviceAccountName // "default")"
+ " automountServiceAccountToken=\(if .spec.automountServiceAccountToken == null then "unset" else .spec.automountServiceAccountToken end)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
#
# automate_pod_token_automount_fix.sh
#
# Idempotently set automountServiceAccountToken: false on Pods that
# do NOT call the Kubernetes API, by patching their ServiceAccounts.
#
# REQUIREMENTS:
# - Run on any machine with kubectl and jq installed and kubeconfig set.
# - This script DOES NOT decide which pods call the API.
# You must provide an allowlist of namespaces/pods that legitimately
# call the API and should keep their token.

set -euo pipefail

#------------------------ CONFIGURATION --------------------------------#

# Comma-separated list of namespaces that contain API-calling workloads
# whose pods should NOT be remediated (tokens kept as-is).
API_CALLER_NAMESPACES="kube-system"

# Comma-separated list of pod name regexes that call the API and should
# NOT be remediated (across any namespace), for example:
# "metrics-server|external-dns|cluster-autoscaler"
API_CALLER_POD_NAME_REGEX=""

#------------------------ HELPER FUNCTIONS -----------------------------#

ns_in_allowlist() {
local ns="$1"
IFS=',' read -r -a arr <<< "${API_CALLER_NAMESPACES}"
for x in "${arr[@]}"; do
[[ -n "${x}" && "${ns}" == "${x}" ]] && return 0
done
return 1
}

pod_in_allowlist_regex() {
local pod="$1"
[[ -z "${API_CALLER_POD_NAME_REGEX}" ]] && return 1
if [[ "${pod}" =~ ${API_CALLER_POD_NAME_REGEX} ]]; then
return 0
fi
return 1
}

#------------------------ DISCOVER TARGET PODS -------------------------#

echo "[INFO] Discovering non-compliant pods (ignoring kube-system, kube-public, kube-node-lease)..."

# Reuse the benchmark logic to find non-compliant pods
non_compliant_json="$(kubectl get pods --all-namespaces -o json | jq '
.items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| select(.spec.automountServiceAccountToken != false)
| {namespace: .metadata.namespace,
name: .metadata.name,
serviceAccount: (.spec.serviceAccountName // "default") }
')"

if [[ -z "${non_compliant_json}" ]]; then
echo "[INFO] No pods found (cluster empty?). Nothing to do."
exit 0
fi

targets="$(
printf '%s\n' "${non_compliant_json}" | jq -r '
select(.namespace != null)
| "\(.namespace) \(.name) \(.serviceAccount)"
' | sort -u
)"

if [[ -z "${targets}" ]]; then
echo "[INFO] No non-compliant pods detected. Nothing to do."
exit 0
fi

echo "[INFO] Candidate non-compliant pod tuples (ns pod serviceAccount):"
echo "${targets}"

#------------------------ PATCH SERVICEACCOUNTS ------------------------#

echo "[INFO] Patching ServiceAccounts (setting automountServiceAccountToken=false) where appropriate..."

while read -r ns pod sa; do
[[ -z "${ns}" ]] && continue

# Skip namespaces that you consider API callers
if ns_in_allowlist "${ns}"; then
echo "[SKIP] ${ns}/${pod}: namespace in API_CALLER_NAMESPACES"
continue
fi

# Skip pods matching explicit allowlist regex
if pod_in_allowlist_regex "${pod}"; then
echo "[SKIP] ${ns}/${pod}: pod matches API_CALLER_POD_NAME_REGEX"
continue
fi

# Ensure ServiceAccount exists
if ! kubectl get serviceaccount "${sa}" -n "${ns}" >/dev/null 2>&1; then
echo "[WARN] ${ns}/${pod}: ServiceAccount ${sa} not found, skipping"
continue
fi

# Check current SA setting; avoid unnecessary patch
current="$(kubectl get serviceaccount "${sa}" -n "${ns}" -o json | jq -r '.automountServiceAccountToken // "unset"')"
if [[ "${current}" == "false" ]]; then
echo "[OK] ${ns}/sa/${sa}: automountServiceAccountToken already false"
continue
fi

echo "[ACT] Patching ServiceAccount ${ns}/${sa} (set automountServiceAccountToken=false)..."
kubectl patch serviceaccount "${sa}" -n "${ns}" \
--type merge \
-p '{"automountServiceAccountToken": false}' >/dev/null

done <<< "${targets}"

#------------------------ VERIFICATION ---------------------------------#

echo "[INFO] Waiting briefly for pods to be recreated with new SA settings (if controllers roll them)..."
sleep 10

echo "[INFO] Re-running benchmark-style audit to verify compliance..."

kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| (.spec.automountServiceAccountToken == false) as $ok
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " serviceAccount=\(.spec.serviceAccountName // "default")"
+ " automountServiceAccountToken=\(if .spec.automountServiceAccountToken == null then "unset" else .spec.automountServiceAccountToken end)"
+ " is_compliant=\(if $ok then "true" else "false" end)"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'