Skip to main content
RunBook Academy

KubernetesLVII · AuthenticationAuthentication

Projected ServiceAccount tokens — TokenRequest and audience

Advanced⏱ ~16 minkubectl

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

Not yet marked complete on this device.

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.

ConsumerAudience
API serverhttps://kubernetes.default.svc
Vaultvault://prod.example.com
Cloud provider (IRSA)sts.amazonaws.com
Cloud provider (Workload Identity)https://iam.googleapis.com/
Custom APIthe 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

  1. Token audience mismatch. A workload requests a token with aud=vault, then tries to call the API server. The API server rejects the token with audience mismatch. The fix is to request a token with the API server’s audience (kubernetes).
  2. 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.
  3. ServiceAccount does not exist. A workload references a SA that has been deleted. The kubelet cannot mount a token; the Pod fails to start.
  4. 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.
  5. TokenRequest denied by admission. A ValidatingAdmissionPolicy denies token issuance to the SA. The workload sees a 403 from TokenRequest.

Production failure modes

  1. 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).
  2. 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=15m and request a new token per pipeline run.
  3. 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.
  4. 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

  1. Q1. What is the role of the `audience` claim in a projected ServiceAccount token?

  2. Q2. Projected ServiceAccount tokens mounted into a Pod are rotated automatically by the kubelet before they expire.

  3. 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`.

  4. 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.