Every Kubernetes Pod gets an API token for its service account unless someone explicitly turns it off, so most clusters run hundreds of workloads holding credentials they never use. If an attacker gets code execution in one of those containers, the token at /var/run/secrets/kubernetes.io/serviceaccount/token lets them call the API server as that service account. This runbook shows how to find which Pods actually have the token mounted, separate the ones that need it from the ones that do not, switch the mount off safely and stop new Pods from getting it by default. It is for platform engineers and cluster administrators on self-managed or managed Kubernetes. For background on how these tokens work, see the Kubernetes Service Account Token Guide.
This is an independent guide from NHI Mgmt Group, based on the Kubernetes project's public documentation as of November 2026. Behaviour can change between Kubernetes releases, so confirm against the documentation for your version.
Key takeaways
- Mounting is the default. The ServiceAccount admission controller adds a projected volume named
kube-api-access-<suffix>to every new Pod unless the ServiceAccount or the Pod setsautomountServiceAccountToken: false. - The Pod setting wins. When both the ServiceAccount and the Pod spec set the field, the Pod spec takes precedence, so check both and trust the Pod's volumes as the final answer.
- Look at volumes, not just flags. The reliable test is whether the running Pod has a projected volume with a
serviceAccountTokensource; the flags only explain why. - Most workloads never call the API server. Use audit logs and RBAC lookups to prove which service accounts are used, then disable the mount for the rest.
- Changes only affect new Pods. Admission runs when a Pod is created, so you must restart workloads, and you can roll back by restoring the field and restarting again.
When you need this
- Cluster hardening: the Kubernetes RBAC good practices page advises avoiding the default automounting of service account tokens by setting
automountServiceAccountToken: false. - After a container compromise or vulnerable image report: any Pod with a mounted token gives an attacker an authenticated starting point. MITRE ATT&CK tracks this as T1528 Steal Application Access Token and T1552.007 Container API. The Kubeflow cryptomining attacks show how a mounted service account turns one exposed workload into control of the cluster.
- Before tightening RBAC: knowing which Pods hold tokens tells you where an over-broad RoleBinding is reachable from inside a container.
- Audit evidence: least privilege controls such as NIST SP 800-53 AC-6 expect you to show that credentials are issued only where needed.
Before you start
Understand what the token mount is before you go looking for it. According to the Kubernetes documentation, when neither field is false the admission controller adds a projected volume with three sources and mounts it into each container at /var/run/secrets/kubernetes.io/serviceaccount, skipping containers that already mount that path.
| File in the mount | Source in the projected volume | What it gives the container |
|---|---|---|
token | serviceAccountToken, expirationSeconds: 3607 | A JWT bound to the Pod, audience the API server, renewed by the kubelet; it expires when the Pod is deleted or after its lifespan (one hour by default) |
ca.crt | ConfigMap kube-root-ca.crt | The cluster CA bundle, so the client can verify the API server |
namespace | Downward API metadata.namespace | The Pod's namespace name |
The token is short-lived, but the kubelet keeps refreshing it for as long as the Pod runs, so an attacker inside the container always has a valid one. Kubernetes versions before 1.22 mounted a long-lived token from a Secret instead; legacy Secrets of type kubernetes.io/service-account-token may still exist and are covered in How to rotate a Kubernetes service account token and detect old token use.
You need:
- Read access:
getandliston Pods, ServiceAccounts, RoleBindings and ClusterRoleBindings across namespaces. Run discovery from a read-only identity, not cluster-admin. - Tools:
kubectlandjq. The jq filters below read documented Pod fields; adjust them to your shell. - Audit logs: API server audit logging at Metadata level or above, retained for at least 30 days, so you can see which service accounts call the API. Managed services expose these through their logging service. See the Kubernetes Audit Log Detection Guide.
- An owner list: who owns each namespace, so you can confirm before you change anything.
Step-by-step
Steps 1 to 5 are read-only. Steps 6 and 7 change configuration and are reversible by restoring the field and restarting Pods.
- List running Pods that actually have a service account token volume. This is the ground truth, because it reflects what admission did when each Pod was created:
Volumes namedkubectl get pods -A -o json | jq -r ' .items[] | select([.spec.volumes[]?.projected.sources[]?.serviceAccountToken] | map(select(. != null)) | length > 0) | [.metadata.namespace, .metadata.name, (.spec.serviceAccountName // "default")] | @tsv'kube-api-access-*are the automounted ones. Other projected volumes with aserviceAccountTokensource were added on purpose, often for a non-API audience such as a cloud workload identity integration or a service mesh. Record both, but treat them separately. - Find legacy Secret-based token mounts. List Secrets of the legacy type, then check whether any Pod mounts them as a volume:
Thekubectl get secrets -A -o json | jq -r ' .items[] | select(.type == "kubernetes.io/service-account-token") | [.metadata.namespace, .metadata.name, (.metadata.labels["kubernetes.io/legacy-token-last-used"] // "-")] | @tsv'kubernetes.io/legacy-token-last-usedlabel is written by the API server when legacy token tracking is active. These tokens do not expire, so they rank above automounted tokens in your clean-up order. - Read the settings that explain the result. List the ServiceAccount and Pod values using the documented
-o jsonpathrange syntax:
An empty column means the field is unset, which behaves as true. Use the decision table below to classify each Pod.kubectl get serviceaccounts -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.automountServiceAccountToken}{"\n"}{end}' kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\t"}{.spec.automountServiceAccountToken}{"\n"}{end}' - Find out what each service account can do. For every service account behind a mounted token, list its effective permissions by impersonating it:
Thekubectl auth can-i --list --namespace <ns> --as system:serviceaccount:<ns>:<sa>defaultServiceAccount in each namespace has no permissions beyond the API discovery permissions granted to all authenticated principals, unless someone has bound a role to it. Any binding to adefaultaccount is a finding in its own right. Pair this with How to Audit kubernetes cluster admin bindings to rank Pods whose token reaches cluster-admin or Secret read access. - Prove which tokens are used. Search your audit logs for requests whose
user.usernameissystem:serviceaccount:<ns>:<sa>over your review window. Bound tokens carry private claims such asauthentication.kubernetes.io/pod-nameandauthentication.kubernetes.io/credential-idin the authenticated user's extra fields, which lets you tie a call to a specific Pod. A service account with a mounted token and no API calls in 30 to 90 days is a candidate for disabling. Confirm with the owner: some controllers only call the API during failover or scaling events. - Disable the mount where it is not needed. Prefer the ServiceAccount level for dedicated accounts and every
defaultaccount, and the Pod template level for workloads that share an account:
For a Deployment, setapiVersion: v1 kind: ServiceAccount metadata: name: default namespace: <ns> automountServiceAccountToken: falsespec.template.spec.automountServiceAccountToken: falsein the manifest held in source control, not with a live patch, so your GitOps tooling does not revert it. - Restart the workloads. Admission only acts when a Pod is created, so existing Pods keep their tokens until replaced. Roll out in rings: one non-production namespace, then low-risk production namespaces, then the rest. Use your normal deployment process or
kubectl rollout restart deployment/<name> -n <ns>, and watch for errors before moving on.
Decision table
| ServiceAccount field | Pod field | Token mounted? | Action |
|---|---|---|---|
| unset or true | unset | Yes | Default case. Disable unless audit logs show API use |
| false | unset | No | Target state for most accounts |
| false | true | Yes | Deliberate opt-in. Confirm the workload needs it and its RBAC is narrow |
| unset or true | false | No | Fine for that Pod, but other Pods using the account still get tokens |
| any | any, with an explicit projected serviceAccountToken volume | Yes, custom | Check the audience and path; not controlled by the automount field |
Which workloads usually need the token
- Controllers and operators: anything that watches or changes Kubernetes objects, such as ingress controllers, cert managers, autoscalers and custom operators. See the Kubernetes Operator Security guide.
- Agents in system namespaces: CNI, CSI, logging and monitoring DaemonSets often read node or Pod metadata. Do not change accounts in
kube-systemor vendor-managed namespaces without the vendor's guidance. - CI runners and job launchers: they create Pods or Jobs, so their tokens are powerful and deserve the tightest RBAC.
- Ordinary application Pods: web services, APIs, batch jobs and databases rarely call the Kubernetes API. These are the bulk of the clean-up.
Verify it worked
- Re-run step 1. The changed workloads should no longer show a
kube-api-access-*volume after their Pods are replaced. - Inspect one Pod:
kubectl get pod <name> -n <ns> -o yamlshould show no projected token volume and no mount at/var/run/secrets/kubernetes.io/serviceaccount. - Check the application: readiness probes pass, error rates are flat and logs show no authentication failures to the API server.
- Check audit logs: a sudden burst of 401 responses for a
system:serviceaccountuser after the change means a workload did need its token.
Rollback and common errors
Rollback: set automountServiceAccountToken: true on the Pod template (which overrides the ServiceAccount) or remove the false value from the ServiceAccount, then restart the workload. No data is lost: the token is minted fresh for each new Pod.
| Symptom | Likely cause | Fix |
|---|---|---|
| Application logs show it cannot read the token file or load in-cluster config | The workload uses a Kubernetes client library and did need the token | Opt the Pod back in with the Pod-level field, and give it a dedicated ServiceAccount with narrow RBAC |
| Change made but Pods still have the volume | Pods were not recreated, or the Pod template sets the field to true | Restart the workload; check the Pod template overrides |
| Field reverts after a few minutes | GitOps or a Helm release reconciles the live object back | Make the change in the chart values or manifests in source control |
| Pods still get a token volume with a different name | A webhook or chart adds its own projected serviceAccountToken volume | Review that integration separately; the automount field does not control it |
| Pod creation rejected after a policy rollout | An enforcing admission policy blocks Pods that request the token | Add a documented exception for the workload or switch the policy to audit mode while you fix it |
Hardening afterwards
- Make false the default for new namespaces. An admission policy can set the field on every new
defaultServiceAccount. The Kyverno community policydisable-automountserviceaccounttokenis one published example; a ValidatingAdmissionPolicy or Gatekeeper constraint can instead report or block Pods that mount tokens without an approved label. Start in audit mode. See the Kubernetes Admission Control Guide. - Give every API-calling workload its own ServiceAccount. Shared accounts make it impossible to disable the mount for one workload without touching others, and they blur audit trails.
- Keep RBAC narrow for the accounts that keep tokens. The Kubernetes RBAC good practices page warns that the right to create workloads in a namespace implicitly grants the API access of any service account in it. See the Kubernetes RBAC Guide.
- Remove legacy token Secrets. Delete unused Secrets of type
kubernetes.io/service-account-tokenonce tracking shows no use; the controller manager's legacy token cleaner marks and deletes unused auto-generated ones after one year by default. - Repeat the inventory on a schedule. Run steps 1 to 4 monthly and on every new namespace, and feed the result into your NHI inventory. See the Kubernetes NHI Security Guide.
Choosing where to set the field
| Where | Best for | Trade-off |
|---|---|---|
| ServiceAccount | default accounts and dedicated accounts whose Pods never call the API | One change covers every Pod using the account; a Pod can still opt back in |
| Pod template | Shared accounts, or a single workload that differs from the rest | Must be repeated in every chart and manifest; easy to miss |
| Admission policy | Large estates with many teams | Needs a policy engine and an exception process; enforce only after audit mode is clean |
Small teams with a handful of namespaces can set the ServiceAccount field by hand and stop there. Larger organisations should combine the ServiceAccount default with an admission policy, because new namespaces and new charts appear faster than reviews.
Metrics
- Token exposure rate: percentage of running Pods with a
kube-api-access-*volume. Expect it to fall to the share of Pods that run controllers or agents. - Default account exposure: number of Pods running as a
defaultServiceAccount with a mounted token. Target zero. - Unused mounted tokens: service accounts with mounted tokens and no audit log activity in 90 days.
- Privileged reach: Pods whose mounted token can read Secrets or create Pods cluster-wide.
- Legacy token Secrets: count of
kubernetes.io/service-account-tokenSecrets remaining.
Framework mapping
| Framework | Reference | How this runbook supports it |
|---|---|---|
| NIST SP 800-53 Rev. 5 | AC-6 Least Privilege; IA-5 Authenticator Management; CM-6 Configuration Settings | Issue credentials only to workloads that need them; enforce a secure default |
| NIST CSF 2.0 | PR.AA-05; PR.PS-01; DE.CM-09 | Least privilege for workload access, configuration management, monitoring of runtime environments |
| CIS Controls v8.1 | 5.4 Restrict Administrator Privileges to Dedicated Administrator Accounts; 6.8 Define and Maintain Role-Based Access Control | Narrow and documented access for service accounts |
| ISO/IEC 27001:2022 Annex A | A.5.18 Access rights; A.8.2 Privileged access rights | Access rights provisioned and reviewed per workload |
| MITRE ATT&CK | T1528 Steal Application Access Token; T1552.007 Container API; T1613 Container and Resource Discovery | Removes the token an attacker would steal and use for discovery |
Practitioner checklist
- List every running Pod with a projected
serviceAccountTokenvolume and separate automounted from custom ones. - List legacy
kubernetes.io/service-account-tokenSecrets and the Pods that mount them. - Record the ServiceAccount and Pod
automountServiceAccountTokenvalues and classify each Pod. - Check effective permissions of each mounted account with
kubectl auth can-i --list. - Use audit logs to prove which service accounts call the API over 30 to 90 days.
- Set the field to false on every
defaultServiceAccount and on unused dedicated accounts, in source control. - Restart workloads in rings and watch for 401 errors and failed probes.
- Add an admission policy in audit mode, then enforce, with a documented exception route.
- Track token exposure rate and default account exposure monthly.
Standards and references
- Kubernetes: Configure Service Accounts for Pods
- Kubernetes: Managing Service Accounts
- Kubernetes: Service Accounts
- Kubernetes: Role Based Access Control Good Practices
- Kubernetes: Authorization (kubectl auth can-i)
- Kubernetes: JSONPath Support
- MITRE ATT&CK: Unsecured Credentials: Container API (T1552.007)
- Kyverno Policies: Disable automountServiceAccountToken
Related NHI Mgmt Group resources: Kubernetes Service Account Token Guide · How to rotate a Kubernetes service account token and detect old token use · How to Audit kubernetes cluster admin bindings · Kubernetes Workload Identity Checklist · Rotating a Windows or Linux Service Account Password