Service Account Tokens Are Only Mounted Where Necessary
More Info:
Pods that do not need to access the Kubernetes API should not mount service account tokens. Disabling token automounting reduces the blast radius of a compromised pod.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS AKS
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
List service accounts with default token behavior
- Run on: any machine with kubectl access
kubectl get serviceaccounts -A -o jsonpath='{range .items[?(@.automountServiceAccountToken!=false)]}{.metadata.namespace}{"\t"}{.metadata.name}{"\n"}{end}'- This shows service accounts that are not explicitly set to
automountServiceAccountToken: false(including those inheriting the cluster default).
-
Identify workloads that will mount tokens via their service accounts
- Run on: any machine with kubectl access
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\t"}{.spec.automountServiceAccountToken}{"\n"}{end}' \| sort- Note: If
.spec.automountServiceAccountTokenis empty, the pod uses the behavior of its referenced service account (or namespace/cluster default).
-
Review which pods actually need API access
- For a sample of pods from step 2 (start with those in non-system namespaces), inspect their full spec and containers to determine if they interact with the Kubernetes API (client libraries, kubeconfig usage,
KUBERNETES_SERVICE_HOST/KUBERNETES_PORTenv usage, etc.):
# Example for a specific podkubectl -n <namespace> get pod <pod-name> -o yaml- Decide per workload: “Requires Kubernetes API access” vs “Does NOT require Kubernetes API access.”
- For a sample of pods from step 2 (start with those in non-system namespaces), inspect their full spec and containers to determine if they interact with the Kubernetes API (client libraries, kubeconfig usage,
-
Update workload templates to disable token automount where not needed
- For each deployment/statefulset/daemonset/job whose pods do NOT need API access, edit its manifest (or use
kubectl edit) to addautomountServiceAccountToken: falseto the pod template: - Run on: any machine with kubectl access
kubectl -n <namespace> edit deployment <deployment-name>- In the opened YAML, under
spec.template.spec, ensure:
spec:automountServiceAccountToken: false- Save and exit; the controller will recreate pods with tokens disabled.
- For each deployment/statefulset/daemonset/job whose pods do NOT need API access, edit its manifest (or use
-
Optionally harden service accounts for non-API workloads
- For service accounts used only by non-API workloads, set
automountServiceAccountToken: falseon the service account itself (so future pods using it do not mount tokens by default):
kubectl -n <namespace> patch serviceaccount <sa-name> \-p '{"automountServiceAccountToken": false}' - For service accounts used only by non-API workloads, set
-
Verify that service account tokens are not mounted where unnecessary
- Re-run the pod inspection to confirm
automountServiceAccountToken: falseis set on pod specs that should not have tokens:
kubectl get pods -n <namespace> -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.automountServiceAccountToken}{"\n"}{end}'- For a spot check, confirm the token volume is absent inside a pod that should not have a token:
POD=<pod-name>; NS=<namespace>kubectl -n "$NS" exec "$POD" -- ls -R /var/run/secrets/kubernetes.io 2>/dev/null- No files found there (or the directory missing) indicates the token is not mounted.
- Re-run the pod inspection to confirm
Using kubectl
# 1) List all ServiceAccounts and see which have automountServiceAccountToken explicitly set
# Run on: any machine with kubectl access
kubectl get serviceaccounts -A -o yaml \
| sed -n '/^apiVersion: v1$/,/^status:/{/kind: ServiceAccount/p;/metadata:/p;/automountServiceAccountToken:/p}' \
| sed '/^$/d'
Review guidance:
- If a ServiceAccount has
automountServiceAccountToken: false, pods using it will not auto-mount tokens unless overridden at pod level. - If the field is missing or
true, pods using that ServiceAccount will auto-mount tokens unless the pod spec disables it.
# 2) Find pods that explicitly control automountServiceAccountToken
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\t"}{.spec.automountServiceAccountToken}{"\n"}{end}' \
| column -t
Review guidance:
- The 4th column is the pod-level setting:
false→ token will not be mounted for that pod (good for workloads that do not need API access).true→ token will be mounted (needs review: does this pod really need API access?).<no value>(blank) → pod inherits behavior from its ServiceAccount; see step 1.
Focus review on:
- Pods whose function clearly does not need Kubernetes API access but show
trueor blank with atrue/unset ServiceAccount. - High-privilege or internet-facing workloads where reducing token exposure is especially important.
# 3) For a specific workload type (example: Deployments), inspect pod templates
# Replace NAMESPACE and DEPLOYMENT with real names
kubectl -n NAMESPACE get deployment DEPLOYMENT -o yaml \
| sed -n '/spec:/,/status:/{/serviceAccountName:/p;/automountServiceAccountToken:/p}'
Review guidance:
- In
.spec.template.specof the workload:automountServiceAccountToken: false→ template is configured to avoid mounting tokens.- Missing or
true→ template will mount tokens (or inherit from ServiceAccount), and should be reviewed for necessity.
Use this pattern for other controllers (StatefulSet, DaemonSet, CronJob, Job) by swapping the resource kind in the command.
Automation
#!/usr/bin/env bash
# Report pods and serviceaccounts that may be unnecessarily mounting service account tokens.
# Run on any machine with kubectl context for the target cluster.
set -euo pipefail
echo "=== 1) Namespaces with default ServiceAccount automount set to true or unset (defaults to true) ==="
echo "NAMESPACE,SA,SA_AUTOMOUNT"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
sa: .metadata.name,
automount: ( .automountServiceAccountToken // "default(true)" )
}
| select(.automount == true or .automount == "default(true)")
| "\(.ns),\(.sa),\(.automount)"
' | sort
echo
echo "=== 2) Pods that explicitly automount the service account token ==="
echo "NAMESPACE,POD,CONTROLLER_KIND,CONTROLLER_NAME,POD_AUTOMOUNT,SA,SA_AUTOMOUNT"
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
pod: .metadata.name,
sa: (.spec.serviceAccountName // "default"),
podAutomount: (
.spec.automountServiceAccountToken
// "inherit-from-SA/namespace-default"
),
owner: (
.metadata.ownerReferences[0]
// {kind:"",name:""}
)
}
| . + {
saAutomount: (
.sa as $sa
| .ns as $ns
| (input | .items[] | select(.metadata.namespace==$ns and .metadata.name==$sa) |
(.automountServiceAccountToken // "default(true)"))
)
}
' <(kubectl get sa --all-namespaces -o json) \
| awk -F'\t' 'BEGIN{OFS=","}{
# jq outputs tab-separated fields in this order due to object literal:
# ns, pod, sa, podAutomount, owner.kind, owner.name, saAutomount
print $1,$2,$5,$6,$4,$3,$7
}' | sort
echo
echo "=== 3) Pods that are LIKELY NOT needing API access: no known API client in image/args (heuristic ONLY) ==="
echo "NAMESPACE,POD,CONTROLLER_KIND,CONTROLLER_NAME,POD_AUTOMOUNT,SA,SA_AUTOMOUNT,IMAGES"
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
pod: .metadata.name,
sa: (.spec.serviceAccountName // "default"),
podAutomount: (
.spec.automountServiceAccountToken
// "inherit-from-SA/namespace-default"
),
owner: (
.metadata.ownerReferences[0]
// {kind:"",name:""}
),
images: (
([.spec.containers[], (.spec.initContainers // [])[]?]
| map(.image)
| unique
| join(";"))
),
cmd: ([.spec.containers[], (.spec.initContainers // [])[]?]
| map((.command + .args) // [])
| add // []
)
}
| . + {
saAutomount: (
.sa as $sa
| .ns as $ns
| (input | .items[] | select(.metadata.namespace==$ns and .metadata.name==$sa) |
(.automountServiceAccountToken // "default(true)"))
)
}
# crude heuristic: filter pods that have SA token auto-mounted (explicitly true or default),
# and whose command/args do NOT mention kubectl, kube, k8s, or in-cluster client libs.
| select(
(
.podAutomount == true
or .podAutomount == "inherit-from-SA/namespace-default"
or .podAutomount == "default(true)"
)
and (
([.cmd[]?] | map(tostring | ascii_downcase) | join(" "))
| test("kubectl|kube-|k8s|kubernetes|incluster|client-go|kubernetes-client") | not
)
)
| "\(.ns),\(.pod),\(.owner.kind // \"\"),\(.owner.name // \"\"),\(.podAutomount),\(.sa),\(.saAutomount),\(.images)"
' <(kubectl get sa --all-namespaces -o json) | sort
How to interpret the output:
-
Section 1:
SA_AUTOMOUNToftrueordefault(true)means pods using that ServiceAccount will mount tokens unless they overridespec.automountServiceAccountToken: false. Review these ServiceAccounts; for workloads that do not need API access, consider settingautomountServiceAccountToken: falseon the ServiceAccount and/or in the pod template. -
Section 2: Lists all pods, showing:
POD_AUTOMOUNT:trueorinherit-from-SA/namespace-defaultmeans the pod is mounting or likely mounting the token.SA_AUTOMOUNT:trueordefault(true)means the ServiceAccount is also allowing mounts. Focus review on pods where:POD_AUTOMOUNTistrueorinherit-from-SA/namespace-default, and- You know the workload does not need to talk to the Kubernetes API.
-
Section 3: Heuristic list of pods that likely do NOT need API access but are auto-mounting tokens (based on simple text matches in command/args). This is only a starting point for human review; it can miss or misclassify workloads. For each candidate, inspect its deployment/statefulset/cronjob and decide whether to set:
spec.automountServiceAccountToken: falsein the pod template, and/orautomountServiceAccountToken: falseon the ServiceAccount.
This script does not attempt to change any resources; all remediation decisions and edits must be made manually in the workload manifests.