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, ...]

More kubernetes tools

Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

Elsewhere on the site