PRACTITIONER GUIDE | CLOUD SECURITY
Practitioner GuideUpdated 13 min read

Kubernetes Service Account Token Security Audit: How to Find Overprivileged Pods and Unused Tokens Before Attackers Use Them for Cluster Escalation

62%
of Kubernetes deployments have at least one pod running with a service account that has ClusterAdmin or equivalent permissions
Default
automountServiceAccountToken is true in Kubernetes by default -- every pod gets an API token unless explicitly disabled
3
common paths from pod compromise to cluster admin: service account ClusterRoleBinding, mounted token used for API access, and node-level token theft
1 hour
is the default expiry for projected service account tokens (bound tokens) introduced in Kubernetes 1.20, versus non-expiring legacy tokens

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

Container escape vulnerabilities are regularly discovered. When an attacker escapes a container, the first thing they do is check what the pod's service account token can access. If the answer is cluster-admin, the cluster is fully compromised from a single pod breakout. The audit described here does not require a container escape to assess this risk. It enumerates all service account RBAC bindings from the control plane and identifies which pods would become a critical escalation path if their container were compromised.

Enumerating All Service Account RBAC Bindings

Start with a complete inventory of what each service account can do:

# List all service accounts across all namespaces
kubectl get serviceaccounts --all-namespaces

# List all ClusterRoleBindings (cluster-wide RBAC)
kubectl get clusterrolebindings -o wide

# Find all ClusterRoleBindings that grant cluster-admin
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name == "cluster-admin") | \
  {name: .metadata.name, subjects: .subjects}'

# List all RoleBindings in all namespaces
kubectl get rolebindings --all-namespaces -o wide

For a comprehensive audit, use kubectl-who-can (a kubectl plugin) to check what each service account can do:

# Install via krew
kubectl krew install who-can

# Find who can create pods (a common privilege escalation vector)
kubectl who-can create pods --all-namespaces

# Find who can exec into pods (another escalation vector)
kubectl who-can create pods/exec --all-namespaces

# Find who can list secrets (secrets may contain credentials)
kubectl who-can list secrets --all-namespaces

Any service account that appears in the output of cluster-admin binding queries or that can create pods, exec into pods, or list secrets is a high-priority finding. Document the workload that uses this service account and evaluate whether the permission is required.

Identifying Pods Running with Overprivileged Service Accounts

After finding overprivileged service accounts, identify which pods are using them:

# Find all pods using a specific service account
kubectl get pods --all-namespaces -o json | \
  jq '.items[] | select(.spec.serviceAccountName == "my-service-account") | \
  {namespace: .metadata.namespace, pod: .metadata.name, \
   sa: .spec.serviceAccountName}'

# Find all pods running with automountServiceAccountToken not disabled
kubectl get pods --all-namespaces -o json | \
  jq '.items[] | select(.spec.automountServiceAccountToken != false) | \
  {namespace: .metadata.namespace, pod: .metadata.name, \
   sa: .spec.serviceAccountName}'

Cross-reference: for each pod with an overprivileged service account, assess the attack surface:

  • Is the pod internet-facing (LoadBalancer or Ingress)?
  • Does the pod run user-supplied code or untrusted inputs?
  • Is the pod running a known-vulnerable image version?
  • Does the pod run as root (check securityContext.runAsNonRoot: false or absent)?

A pod running as root, facing the internet, running a known-vulnerable web framework, and mounted with a cluster-admin token is the highest-risk combination. Prioritize remediating these first.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Disabling Automount Where Tokens Are Not Needed

Most application workloads do not need to communicate with the Kubernetes API. For those pods, disable the automatic token mount.

At the pod spec level:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web-app
spec:
  template:
    spec:
      automountServiceAccountToken: false  # Disable token mount
      containers:
        - name: app
          image: my-app:1.0

At the service account level (applies to all pods using this SA by default):

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-web-app-sa
  namespace: production
automountServiceAccountToken: false

The pod-level setting overrides the service account-level setting, so you can disable at the SA level and re-enable for specific pods that do need API access.

For workloads that do need API access: scope the service account permissions to the minimum required using a Role (namespace-scoped) rather than a ClusterRole:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: configmap-reader
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list"]
    resourceNames: ["app-config"]  # scope to specific named resource

Never use * in resources or verbs for production workload service accounts.

Migrating from Service Account Tokens to Workload Identity

The most common (and dangerous) pattern for Kubernetes workloads that need cloud provider access (AWS, GCP, Azure) is to mount cloud credentials as environment variables or Kubernetes secrets:

env:
  - name: AWS_ACCESS_KEY_ID
    valueFrom:
      secretKeyRef:
        name: aws-credentials
        key: access_key_id

This pattern stores long-lived cloud credentials in Kubernetes secrets, which are base64-encoded (not encrypted) by default, and mounts them in every pod replica. An attacker who reads the secret from any pod gets long-lived cloud credentials.

Workload identity federation eliminates this pattern by allowing pods to authenticate to cloud APIs using their Kubernetes service account token, exchanged for a short-lived cloud provider credential at authentication time.

AWS: EKS Pod Identity or IRSA (IAM Roles for Service Accounts)

# Create an IAM role for the service account
eksctl create iamserviceaccount \
  --name my-app-sa \
  --namespace production \
  --cluster my-cluster \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --approve

The pod then uses the AWS SDK which automatically exchanges the projected service account token for temporary STS credentials. No static credentials required.

GCP: GKE Workload Identity

# Bind a Kubernetes SA to a GCP service account
gcloud iam service-accounts add-iam-policy-binding \
  my-gcp-sa@my-project.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:my-project.svc.id.goog[production/my-app-sa]"

kubectl annotate serviceaccount my-app-sa \
  --namespace production \
  iam.gke.io/gcp-service-account=my-gcp-sa@my-project.iam.gserviceaccount.com

Bound Service Account Tokens vs Legacy Tokens

Kubernetes 1.20 introduced bound service account tokens (projected volumes) which expire and are audience-scoped. Legacy service account tokens (the secret-based tokens in older Kubernetes versions) do not expire and can be used to authenticate to any Kubernetes API server.

Check whether your cluster is using legacy long-lived tokens:

# Find all service account secret-based tokens (legacy non-expiring)
kubectl get secrets --all-namespaces -o json | \
  jq '.items[] | select(.type == "kubernetes.io/service-account-token") | \
  {namespace: .metadata.namespace, name: .metadata.name, \
   serviceAccount: .metadata.annotations["kubernetes.io/service-account.name"]}'

In Kubernetes 1.24+, secret-based tokens are no longer automatically created. If your cluster still has them from a pre-1.24 era, they are long-lived and should be audited.

For bound tokens (the current default): check that your admission controller or Pod Security Standards require projected service account tokens with an expiry. The token path in the pod is typically /var/run/secrets/kubernetes.io/serviceaccount/token, and for bound tokens the audience, expiry, and issuer are validated by the API server on each API call.

Use the kubectl auth can-i command to verify what a specific token is permitted to do:

# Test permissions of a specific service account
kubectl auth can-i --list --as=system:serviceaccount:production:my-app-sa

This lists every resource and verb the service account can perform, which is the definitive permission inventory for that account.

The bottom line

Kubernetes service account tokens are the most commonly over-granted credentials in container environments. Audit ClusterRoleBindings for any service account with cluster-admin or broad secret/pod permissions, disable automount on every pod that does not need API access, migrate cloud credentials to workload identity federation, and run kubectl auth can-i --list for each high-risk service account to get the authoritative permission inventory. Schedule this audit quarterly; RBAC accumulates permissions the same way AD does.

Frequently asked questions

What is the difference between a Role and a ClusterRole in Kubernetes RBAC?

A Role is namespace-scoped: it grants permissions on resources within a specific namespace. A ClusterRole is cluster-scoped: it grants permissions across all namespaces or on cluster-scoped resources (nodes, persistent volumes, ClusterRoleBindings). A RoleBinding can bind either a Role or a ClusterRole to subjects within a namespace. Prefer Roles over ClusterRoles for application workloads unless the workload genuinely needs cross-namespace or cluster-scoped resource access. ClusterRoleBindings (which grant ClusterRole permissions cluster-wide) are the highest-risk RBAC object and should be audited most carefully.

How do I find pods that are mounting the default service account token?

The default service account exists in every namespace. Find pods using it: `kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null) | {ns: .metadata.namespace, pod: .metadata.name}'`. In many clusters, the default service account has no RBAC bindings, making it low-risk. But verify: run `kubectl auth can-i --list --as=system:serviceaccount:NAMESPACE:default` for each namespace to confirm the default SA has no permissions before dismissing it.

Can Kubernetes secrets be encrypted at rest?

Yes, but it is not enabled by default. Kubernetes etcd stores secret data base64-encoded (not encrypted) unless encryption at rest is explicitly configured via the kube-apiserver's `--encryption-provider-config` flag. For managed Kubernetes (EKS, GKE, AKS), enable the provider-managed encryption: in EKS, this is AWS KMS envelope encryption for secrets; in GKE, this is CMEK for etcd. Verify encryption is active by checking the encryption config: any secret retrieved with `kubectl get secret -o yaml` should show base64 data, and the underlying etcd value should be an encrypted blob, not the raw base64.

What Kubernetes RBAC permissions are considered dangerous privilege escalation paths?

The most dangerous permissions: (1) `create pods` in any namespace -- allows launching a pod with a privileged service account or hostPath mount, (2) `create pods/exec` or `get pods/exec` -- allows executing commands in existing pods, (3) `list/get secrets` -- allows reading credentials stored as secrets, (4) `create clusterrolebindings` or `create rolebindings` -- allows granting arbitrary permissions to any identity, (5) `patch deployments` -- allows modifying existing deployments to inject a malicious container. Any service account with these permissions should be treated as a critical privilege escalation risk.

How do I enforce that all pods must use explicitly named service accounts rather than the default one?

Create an OPA Gatekeeper or Kyverno policy that denies pod creation when spec.serviceAccountName is set to 'default' or is unset. In Gatekeeper, write a ConstraintTemplate with a Rego rule that checks `.spec.serviceAccountName != "default"`. In Kyverno, use a validate rule with `pattern: {spec: {serviceAccountName: '?*'}}` combined with a deny rule for the default SA name. Apply the constraint in Warn mode first to identify existing workloads that rely on the default service account, then remediate those before switching to Deny mode. This forces development teams to explicitly declare which service account each workload uses, making permissions auditable.

What Kubernetes RBAC permissions are most dangerous to grant to service accounts?

The highest-risk permissions for service accounts: get/list/watch on secrets (allows reading all secrets in the namespace or cluster), create on pods (allows injecting a privileged pod to escape to the node), create on deployments or daemonsets with hostPID or hostNetwork (allows host-level access), create on clusterrolebindings or rolebindings (allows escalating the service account's own permissions), and impersonate on users or service accounts (allows acting as any other principal). The wildcard verb on any resource is equivalent to cluster admin for that resource. Audit service account bindings using: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.kind=="ServiceAccount")' to enumerate all service accounts with role bindings and the permissions those roles include.

Sources & references

  1. Kubernetes: RBAC Authorization
  2. CNCF: Kubernetes Security Whitepaper
  3. NSA/CISA: Kubernetes Hardening Guide
  4. Aqua Security: Kubernetes RBAC best practices

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.