Kubernetes clusters should disable automounting API credentials is a Microsoft Defender for Cloud recommendation raised when pods have a service account token mounted, even if they never use it. Fix it by setting automountServiceAccountToken: false on each namespace’s default ServiceAccount or in the pod spec, restarting affected pods, and giving workloads that need the API a dedicated, least-privilege ServiceAccount.
What the scanner is actually detecting
The finding comes from Microsoft Defender for Cloud (the Defender for Containers plan) under the exact title Kubernetes clusters should disable automounting API credentials. Microsoft lists it as a Kubernetes data plane recommendation with High severity. It is backed by an Azure Policy definition of the same name, which the Azure Policy add-on (AKS) or the Azure Policy extension (Arc-connected EKS, GKE and self-managed clusters) evaluates inside the cluster through OPA Gatekeeper.
The detail that matters is what the policy looks at: Pod objects, not ServiceAccounts. The Gatekeeper template (constraint kind K8sAzureV2BlockAutomountToken) marks a pod non-compliant when either condition holds:
- the pod spec sets
automountServiceAccountToken: true, or - the pod spec does not set the field at all, and any container, init container or ephemeral container has a volume mounted at
/var/run/secrets/kubernetes.io/serviceaccount.
The violation message reads “Automounting service account token is disallowed, pod:” followed by the pod name. By default the assignment skips the kube-system, gatekeeper-system, azure-arc and azure-extensions-usage-system namespaces.
Why do ordinary pods trip it? Kubernetes creates a ServiceAccount named default in every namespace, and unless told otherwise the ServiceAccount admission controller adds a projected volume to each pod containing an API token, the cluster CA bundle and the pod’s namespace, mounted at exactly that path.
| Configuration | New pods get a token? | Defender result |
|---|---|---|
| Neither ServiceAccount nor pod sets the field | Yes (Kubernetes default) | Non-compliant |
| ServiceAccount set to false, pod field unset | No | Compliant |
| Pod spec set to false | No, whatever the ServiceAccount says | Compliant |
| Pod spec set to true | Yes | Non-compliant |
How serious is it in practice
Any process inside the container can read the token and present it to the API server as the pod’s ServiceAccount. What that achieves depends on RBAC. With RBAC enabled, the default ServiceAccount in each namespace has no permissions beyond API discovery, so on a well-kept cluster a stolen default token lets an attacker look around but not change much.
The finding deserves priority when:
- someone has bound a Role or ClusterRole to a namespace’s
defaultServiceAccount, which the Kubernetes RBAC documentation warns against because every pod in that namespace then inherits those rights; - a workload runs under a ServiceAccount with broad permissions, such as reading Secrets or creating pods;
- the cluster still holds long-lived Secret-based tokens, which Kubernetes used before version 1.22 and which do not expire.
Since Kubernetes 1.22, mounted tokens come from the TokenRequest API, are bound to the pod and expire after one hour by default, with the kubelet refreshing them. That limits reuse after the pod is deleted, but does nothing while an attacker has code execution in the running container. Defender rates this High because it cannot see your RBAC. Treat it as cheap defense in depth everywhere, and as urgent where the ServiceAccount carries real permissions.
How to confirm it on your cluster
List every pod with its ServiceAccount and pod-level setting. An empty last column means the field is unset, so the ServiceAccount decides:
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"t"}{.metadata.name}{"t"}{.spec.serviceAccountName}{"t"}{.spec.automountServiceAccountToken}{"n"}{end}'
List the ServiceAccount-level setting across namespaces:
kubectl get serviceaccounts -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"t"}{.metadata.name}{"t"}{.automountServiceAccountToken}{"n"}{end}'
Check whether a specific pod actually has the token mounted, which is what the policy tests:
kubectl get pod POD_NAME -n NAMESPACE -o jsonpath='{.spec.containers[*].volumeMounts[*].mountPath}'
Find out what the token could do if stolen (you need impersonation rights for --as):
kubectl auth can-i --list --as=system:serviceaccount:NAMESPACE:SA_NAME -n NAMESPACE
With the Azure Policy add-on or extension installed, Gatekeeper also records audit violations in the constraint’s status field:
kubectl get k8sazurev2blockautomounttoken -o yaml
How to fix it
The Kubernetes side of the fix is identical on AKS, EKS, GKE and self-managed clusters. Only the Defender plumbing (add-on versus Arc extension) differs.
Disable automount on each namespace’s default ServiceAccount
For a single namespace:
kubectl patch serviceaccount default -n NAMESPACE -p '{"automountServiceAccountToken": false}'
Across all application namespaces (bash), skipping system namespaces. Extend the skip list with your platform’s own system namespaces:
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
case "$ns" in kube-system|kube-public|kube-node-lease|gatekeeper-system) continue ;; esac
kubectl patch serviceaccount default -n "$ns" -p '{"automountServiceAccountToken": false}'
done
If you manage namespaces through GitOps, declare it instead so it survives rebuilds:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: my-app
automountServiceAccountToken: false
The token mount is decided when a pod is created, so existing pods keep their token until replaced. Restart workloads afterwards, for example every Deployment in a namespace:
kubectl rollout restart deployment -n NAMESPACE
Set it in the pod template
Setting the field in the pod spec is the most explicit option, and when both the pod and the ServiceAccount specify a value, the pod spec takes precedence:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: my-app
spec:
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
automountServiceAccountToken: false
containers:
- name: web
image: myregistry.example.com/web:1.4.2
If the workload is installed from a Helm chart, set the equivalent chart value rather than editing rendered manifests, or the next upgrade will undo it.
Workloads that genuinely call the Kubernetes API
Operators, controllers, CI runners and anything using in-cluster client configuration need a token. Give each one its own ServiceAccount bound to a narrowly scoped Role, never extra rights on default:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-reader
namespace: my-app
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-watcher-read
namespace: my-app
subjects:
- kind: ServiceAccount
name: config-watcher
namespace: my-app
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
These pods will still be flagged, because the policy blocks any mounted token. Handle them as documented exceptions through the policy parameters (Environment settings > Security policies > Manage effect and parameters), using excludedNamespaces or excludedImages. Microsoft advises fully qualified image names, since a trailing * does prefix matching and can exclude more than intended.
Keep it fixed
Once the backlog is clean, change the effect from Audit to Deny (select the recommendation, then Take action > Deny) so new violations are rejected at admission. Also add the ServiceAccount manifest to your namespace provisioning: each new namespace gets a fresh default ServiceAccount without your setting, and if someone deletes default, the control plane recreates it the same way.
How to verify the fix and rescan
After restarting, the pod should show false and no mount at the token path:
kubectl get pod POD_NAME -n NAMESPACE -o jsonpath='{.spec.automountServiceAccountToken}{"n"}{.spec.containers[*].volumeMounts[*].mountPath}{"n"}'
kubectl exec -n NAMESPACE POD_NAME -- ls /var/run/secrets/kubernetes.io/serviceaccount
The second command should fail with “No such file or directory” (it needs ls in the image). On the Defender side, the Azure Policy add-on runs a full cluster audit every 15 minutes, and each compliance report includes violations from the last 45 minutes, so a fixed pod can stay listed for a while. Recheck after an hour or so before assuming the fix failed.
What can break and how to roll back
- In-cluster API clients. The in-cluster configuration in client-go reads the token from
/var/run/secrets/kubernetes.io/serviceaccount/token. Without it, startup fails with “open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory”, and anything built on it, such as controllers and leader election, stops working. - The namespace and CA files disappear too. They live in the same projected volume. Applications that read
/var/run/secrets/kubernetes.io/serviceaccount/namespaceto discover their namespace should switch to a Downward API environment variable (fieldRef: metadata.namespace). - Deny mode. Pods are created by controllers, so rejections show up as FailedCreate events on the ReplicaSet or Job rather than as a failed
kubectl apply, and a rollout can stall with no new pods. Checkkubectl describe rs -n NAMESPACE.
To roll back, restore the token for the affected ServiceAccount (or remove the pod-level field) and restart:
kubectl patch serviceaccount default -n NAMESPACE -p '{"automountServiceAccountToken": true}'
kubectl rollout restart deployment/DEPLOYMENT_NAME -n NAMESPACE
If Deny is blocking deployments, switch the effect back to Audit first. Test the patch on one namespace in staging before looping over the cluster.
Common false positive reasons
- Legitimate API consumers. Operators and controllers are flagged by design. They are expected findings to exclude and document, not configuration mistakes.
- Third-party components. Microsoft notes that components installed outside
kube-systemcan be flagged. Monitoring agents and ingress controllers often need the API. - Stale results. The 15-minute audit cycle and 45-minute reporting window mean recently fixed pods still appear. Completed Job pods also remain Pod objects until cleaned up.
- Pod field overrides the ServiceAccount. A chart that renders
automountServiceAccountToken: truein the pod template keeps the finding open even after you patch the ServiceAccount. - Manually mounted tokens. A hand-written projected token volume at the standard path, with the pod field unset, matches the policy’s path check.
For a broader method of sorting real issues from noise, see our guide to triaging Defender for Cloud recommendations at scale.
FAQ
Which wins if the ServiceAccount and the pod disagree?
The pod spec. Kubernetes documentation states that when both specify automountServiceAccountToken, the pod’s value takes precedence.
Do running pods pick up the change?
No. The token volume is added when the pod is created, so restart or roll out the workload after patching the ServiceAccount.
Should I change the default ServiceAccount in kube-system?
No. The policy excludes kube-system by default, and the components there are managed by your provider or distribution. Changing them risks breaking the cluster and gains nothing for this finding.
Does disabling automount revoke tokens that already exist?
No. Bound tokens expire when their pod is deleted, but long-lived Secret-based tokens remain valid. List them with kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove those you no longer need.
Tracking this finding across many clusters
This recommendation tends to reappear with every new namespace and chart upgrade, so the exceptions are worth recording centrally with a reason and an owner. If you already consolidate scanner output, SITEY is a self-hosted vulnerability management platform (the server runs on Linux) that imports findings from 16 scanners and merges duplicates per scanner; its AI triage suggests false positives with evidence, and a human makes the final call.