KubernetesLVII · AuthenticationAuthentication
Projected ServiceAccount tokens — TokenRequest and audience
What you'll learn
- Explain how projected ServiceAccount tokens are issued and rotated
- Configure audience and expiry to scope a token to a single consumer
- Request a token via the TokenRequest API for CI/CD and external workloads
- Debug common failure modes (token not rotated, audience mismatch, expired token)
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Projected ServiceAccount tokens are the modern replacement for long-lived token Secrets. They are short-lived (1 hour by default), scoped by audience, automatically rotated by the kubelet, and revocable by rotating the signing key. Every workload that authenticates to the API server from inside a Pod — and every CI/CD pipeline that uses the TokenRequest API to issue a token from outside a Pod — should use projected tokens. This lesson covers the issuance, rotation, scoping, and the failure modes that break in production.
How a projected token is issued
A projected token is a JWT signed by the API server’s
--service-account-signing-key-file. The token is
issued by the TokenRequest API endpoint:
POST /apis/authentication.k8s.io/v1/tokenrequests
Content-Type: application/json
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"spec": {
"audiences": ["https://kubernetes.default.svc"],
"expirationSeconds": 3600,
"boundObjectRef": {
"kind": "Pod",
"apiVersion": "v1",
"name": "ci-runner-abc123"
}
}
}
The API server returns:
{
"status": {
"token": "eyJhbGciOiJSUzI1NiIs...",
"expirationTimestamp": "2026-08-16T15:00:00Z"
}
}
The JWT carries:
{
"iss": "https://kubernetes.default.svc",
"sub": "system:serviceaccount:default:ci-runner",
"aud": ["https://kubernetes.default.svc"],
"exp": 1755356400,
"nbf": 1755352800,
"iat": 1755352800,
"kubernetes.io": {
"namespace": "default",
"serviceaccount": {
"name": "ci-runner",
"uid": "..."
}
}
}
The aud claim scopes the token: a token issued with
aud=kubernetes is rejected by anything else; a token
issued with aud=vault.example.com is rejected by the
API server. The exp claim bounds the lifetime.
sequenceDiagram
participant W as Workload (Pod)
participant TR as TokenRequest API
participant AS as API server
W->>TR: POST /tokenrequests (audience, expiry)
TR->>AS: Sign JWT with SA key
AS->>TR: Signed JWT
TR->>W: Token + expiration
W->>AS: GET /api (Authorization: Bearer JWT)
AS->>AS: Verify signature, audience, expiry
AS->>W: 200 OK
Audience scoping
The audience claim is the security primitive of projected tokens. Without it, a token issued for any consumer works against every consumer that shares the signing key. With it, a token works only for the named audience.
| Consumer | Audience |
|---|---|
| API server | https://kubernetes.default.svc |
| Vault | vault://prod.example.com |
| Cloud provider (IRSA) | sts.amazonaws.com |
| Cloud provider (Workload Identity) | https://iam.googleapis.com/ |
| Custom API | the API’s URL or a custom identifier |
A token with aud=sts.amazonaws.com is rejected by the
API server (the server checks aud and finds its own
URL is not in the list). A token with aud=vault is
accepted by Vault but not by anything else.
# ServiceAccount with audience bound
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-issuer
namespace: prod
# Request a token scoped to Vault
kubectl create token vault-issuer \
--audience=vault://prod.example.com \
--duration=10m
The token is rejected by the API server (audience mismatch) but accepted by Vault (if Vault is configured to validate the JWT signature and audience).
Rotation
The kubelet rotates mounted tokens before they expire. The default rotation threshold is the token’s expiry minus 80% of its lifetime. For a 1-hour token, the kubelet rotates after 48 minutes.
gantt
title Token rotation timeline
dateFormat HH:mm
axisFormat %H:%M
section Token lifecycle
Token issued :a1, 10:00, 12m
First rotate :milestone, 10:48, 0m
Token 2 issued :a2, after a1, 60m
Second rotate :milestone, 11:48, 0m
Token 3 issued :a3, after a2, 60m
Token expires :milestone, 12:00, 0m
The rotation is invisible to the workload; the file at
/var/run/secrets/kubernetes.io/serviceaccount/token
is replaced atomically. A workload that reads the file
once and caches it may see an expired token after
rotation; the fix is to re-read the file before each
request or to use a client library that does this
automatically.
TokenRequest for CI/CD
CI/CD pipelines that run outside the cluster can request projected tokens via the API server:
TOKEN=$(kubectl create token ci-runner \
--audience=https://kubernetes.default.svc \
--duration=15m)
kubectl --token="$TOKEN" get pods -n ci
The --duration flag is capped by the API server’s
--service-account-max-token-expiration (default 1
hour 6 minutes for bound tokens; up to 24 hours for
unbound). A pipeline that requests a 24-hour token gets
a token that lives for 24 hours; a pipeline that
requests a 1-hour token lives for 1 hour.
Common failure modes
- Token audience mismatch. A workload requests a
token with
aud=vault, then tries to call the API server. The API server rejects the token withaudience mismatch. The fix is to request a token with the API server’s audience (kubernetes). - Token expires mid-request. A workload reads the token once, caches it, then sends requests for hours. The token expires; subsequent requests fail. The fix is to re-read the token before each request, or to use a client that does this automatically.
- ServiceAccount does not exist. A workload references a SA that has been deleted. The kubelet cannot mount a token; the Pod fails to start.
automountServiceAccountToken: false. The ServiceAccount is configured to not mount a token; the file at/var/run/secrets/kubernetes.io/serviceaccount/is empty. A workload that depends on the file fails.- TokenRequest denied by admission. A ValidatingAdmissionPolicy denies token issuance to the SA. The workload sees a 403 from TokenRequest.
Production failure modes
- Workloads cache the token across rotations. A client library that reads the file once and caches the value breaks at the first rotation. Use a client that re-reads (most do).
- CI/CD tokens are long-lived. A pipeline that
requests a 24-hour token has the same
credential-management problem as a long-lived
Secret. Use
--duration=15mand request a new token per pipeline run. - Tokens shared across clusters. A token signed by
cluster A’s CA is rejected by cluster B (different
signing key, different
iss). A multi-cluster workload that expects to be accepted by both clusters fails. - No audience scoping. A token with no audience
claim works against every consumer that trusts the
signing key. Always set
--audience.
Cross-course references
- The Linux course covers JWT signing and the key management that the API server uses.
- The Observability course covers the audit log entries for projected token requests.
Quiz
Knowledge check · 4 questions
Q1. What is the role of the `audience` claim in a projected ServiceAccount token?
Q2. Projected ServiceAccount tokens mounted into a Pod are rotated automatically by the kubelet before they expire.
Q3. Your CI pipeline requests a token with `kubectl create token ci-runner` (no audience). The pipeline then calls the API server with `kubectl --token=$TOKEN get pods -n staging`. The API server returns 401. Why, and how do you fix it?
The CI runner is in the `ci` namespace. The ServiceAccount is `ci-runner`. The pipeline is using `--token=$TOKEN` where TOKEN came from `kubectl create token ci-runner`. The error message from the API server is `Unauthorized: invalid bearer token`.
Q4. Name three common failure modes of projected ServiceAccount tokens and the fix for each.
Passing score: 75%. Answers are checked in this browser.
Production discipline
Projected ServiceAccount tokens are the modern
credential for Kubernetes workloads. Every workload
authenticates with a short-lived, audience-scoped,
kubelet-rotated token. Every CI/CD pipeline requests a
new token per run, scoped to the API server, with a
duration matching the pipeline’s runtime. A cluster
that has long-lived ServiceAccount token Secrets
(type=kubernetes.io/service-account-token) has a
credential-management failure that should be fixed
before any other security work. The TokenRequest API
is the right primitive for every token issuance; the
audience claim is the right scoping; the kubelet is
the right rotation mechanism.