Kubernetes ServiceAccount Token Decoder

Paste a ServiceAccount token and find out which kind it is, who it authenticates as, which audience will accept it, and how long it has left. A legacy token has no expiry and can only be withdrawn by deleting its Secret, which is the thing worth knowing before anything else.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Decode. Nothing leaves this tab.

Wanted a different tool?

  • JWT Decoder & Inspector if the token did not come from a cluster, because that reads any JWT without expecting the Kubernetes claims.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A legacy token

A token with no expiry, which is a permanent credential in a Secret

eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.sig

A bound token

A projected token with an audience and an expiry, which is the modern default

eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjIl0sImV4cCI6MTc5OTk5OTk5OSwiaXNzIjoiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YXBpIn0.sig

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Using a legacy Secret-based token

    It never expires. Anyone who obtains it holds that service account's permissions until the Secret is deleted, and nothing rotates it.

    Instead:Use projected bound tokens, the default since 1.24, which are short-lived and audience-bound.

  2. Reading permissions out of the token

    The token identifies the service account and carries no permissions. What it can do is decided by RoleBindings evaluated at request time.

    Instead:Use kubectl auth can-i --as=system:serviceaccount:ns:name to see the real answer.

  3. Mounting the default service account token everywhere

    Every pod gets it unless told otherwise, so a compromised container can talk to the API server as that account.

    Instead:Set automountServiceAccountToken: false unless the pod actually calls the API.

What it catches

A generic JWT decoder prints these claims and leaves you to know what they mean. These four are the ones that change what you do next, and they run in this tab: the token is a live credential and does not leave the page.

Legacy tokens, which never expire

A token from a Secret of type kubernetes.io/service-account-token has no exp claim at all. It is a permanent credential for a cluster identity, and the only thing that revokes it is deleting the Secret. That is reported as critical rather than as a missing field, because a copy of it in an old CI log is still a working login today.

The audience, which decides who will accept it

A token minted for sts.amazonaws.com or for a GKE workload identity pool returns 401 against the Kubernetes API, by design. The audience is compared with the issuer, since the API server's --api-audiences defaults to its --service-account-issuer, and the recognised external audiences are named rather than reported as odd.

Expiry, and the rotation point nothing prints

exp, iat and nbf are read out as dates with the time left. For a projected token the 80 percent mark is shown too, because that is when the kubelet rewrites the file, and a process that read the token once at startup is holding a string that stops working while a valid one sits on disk.

What the token cannot tell you, said plainly

There are no permissions in a Kubernetes token. The groups RBAC evaluates, system:serviceaccounts and system:serviceaccounts:<namespace>, are added by the authenticator and are not claims. The page says so and points at kubectl auth can-i --list rather than guessing.

What is actually inside a ServiceAccount token

It is a JWT, so any decoder will print the claims. The two things that decide what to do next are not obvious from the claims alone: which of the two token mechanisms produced it, and which system was meant to accept it. Get the first wrong and you leave a permanent credential in a cluster. Get the second wrong and you spend an afternoon on a 401 that is working exactly as intended.

Two kinds of token, with the same job and opposite risk

Before 1.24, creating a ServiceAccount also created a Secret holding a token with no expiry. Since 1.24 the default is the TokenRequest API: the kubelet projects a short-lived token into the pod and refreshes it. Both authenticate the same way, and you can tell them apart by their claims.

legacy, from a Secret          bound, from TokenRequest
--------------------           ------------------------
iss: kubernetes/serviceaccount iss: https://kubernetes.default.svc...
sub: system:serviceaccount:... sub: system:serviceaccount:...
kubernetes.io/serviceaccount/  aud: [ ... ]
  namespace: prod              exp: 1735689600
  secret.name: api-token-7fk2x iat: 1735686000
  service-account.name: api    kubernetes.io:
  service-account.uid: 0d1a...   namespace: prod
                                 pod:            {name, uid}
no exp. no aud. never expires.   serviceaccount: {name, uid}
                                 node:           {name, uid}

A legacy token is revoked by deleting the Secret, and by nothing else

There is no expiry to wait for and no revocation list to add it to. Rotating the ServiceAccount does not help, because the token stays valid while the Secret exists. This also means read access to Secrets in a namespace is not read access: it is the ability to become every ServiceAccount in it, permanently.

kubectl delete secret api-token-7fk2x -n prod   <- the only revocation

since 1.24  these Secrets are no longer created automatically
since 1.29  auto-generated ones unused for a year are deleted
            by the legacy token cleanup controller

The audience is what makes a token useless in the wrong place

The API server accepts a token only if its aud matches one of --api-audiences, which defaults to the value of --service-account-issuer. A token minted for an external system therefore cannot be replayed against the cluster, which is the entire point of the audience. When somebody reports that a perfectly valid token gets 401, this is usually why.

aud: ["https://kubernetes.default.svc.cluster.local"]
    the default audience: works against the API server

aud: ["sts.amazonaws.com"]
    IRSA. Exchanged at AWS STS for role credentials.
    401 against the Kubernetes API, deliberately.

aud: ["myproject.svc.id.goog"]      GKE Workload Identity
aud: ["api://AzureADTokenExchange"] Entra federation
aud: ["istio-ca"]                   Istio workload certificates

sub names the identity, and the permissions are somewhere else entirely

sub is system:serviceaccount:<namespace>:<name>, and that is the username the API server logs. The groups that RBAC actually evaluates are derived by the authenticator from the same claims, not carried in the token, so no amount of reading the payload tells you what it can do. A RoleBinding to the group system:serviceaccounts:prod grants every ServiceAccount in prod, which is invisible from any single token.

username  system:serviceaccount:prod:api
groups    system:serviceaccounts
          system:serviceaccounts:prod
          system:authenticated

kubectl auth can-i --list --as=system:serviceaccount:prod:api

A projected token is rewritten under the running process

The kubelet refreshes the file when the token is 80 percent through its lifetime, or after 24 hours, whichever comes first. The path stays the same and the contents change. An application that reads the file once at startup keeps the old string, and starts getting 401s at the original expiry while a valid token sits on disk beside it. The official client libraries re-read; shell scripts and hand-rolled HTTP calls usually do not.

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

default lifetime  1 hour (expirationSeconds, minimum 600)
kubelet refresh   at 80 percent of lifetime, or at 24 hours
symptom of a cached read  401 exactly one lifetime after start

Decoding is not verifying, and here it cannot be

Everything a decoder shows is what the token says about itself. A payload can be edited freely and will still decode into a clean-looking table. Proving a token is genuine means checking the signature against the cluster's key, which needs a network request, and this page makes none. The cluster will answer the question properly, and it accounts for revocation as well.

apiVersion: authentication.k8s.io/v1
kind: TokenReview
spec:
  token: eyJhbGciOiJSUzI1NiIsImtpZCI6...

kubectl create -o yaml -f tokenreview.yaml

status.authenticated  true
status.user.username  system:serviceaccount:prod:api
status.user.groups    [system:serviceaccounts, ...]