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
Remediation​
Manual Steps
-
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)"][]' -
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, orcurltohttps://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.
- Inspect container images, args, and env for in-cluster API use:
-
Disable token automount at the Pod spec level for controller-managed workloads (recommended) (run on any machine with kubectl access, adjust
kindas 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}}}}' -
For standalone Pods not managed by a controller, recreate them with token automount disabled (run on any machine with kubectl access):
# Export current Pod speckubectl -n NAMESPACE get pod POD_NAME -o yaml > /tmp/pod-POD_NAME.yaml# Edit the file locallysed -i 's/^spec:/spec:\n automountServiceAccountToken: false/' /tmp/pod-POD_NAME.yaml# Remove status and cluster-assigned fieldsyq -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 Podkubectl -n NAMESPACE delete pod POD_NAMEkubectl -n NAMESPACE apply -f /tmp/pod-POD_NAME.yaml -
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}' -
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=trueor 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:
-
Identify a noncompliant pod and its owner (from the audit output), for example:
- Namespace:
prod - Pod name:
web-abc123 - Owner:
Deployment/prod/web
- Namespace:
-
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
- In
/tmp/deployment-web.yaml, underspec.template.spec, setautomountServiceAccountToken: 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
- Apply the updated manifest:
kubectl apply -f /tmp/deployment-web.yaml
- 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}}'
- 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
- 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'