Overview

Traces how a pod obtains a short-lived, audience-scoped JWT that it uses to authenticate to the Kubernetes API server or external services (IRSA, Workload Identity).

Token Types

TypeMechanismLifetimeAudienceRotation
Legacy SA secret tokenSecret auto-mountedInfinitekubernetes.default.svcNever
Projected volume token (BoundServiceAccountToken)TokenRequest API1 hour (default)configurableAuto by kubelet
External OIDC (IRSA/Workload Identity)Projected volume → federated1 hourAWS/GCP STSAuto by kubelet

Legacy tokens are disabled in Kubernetes 1.24+ by default. All new workloads should use projected volume tokens.

Projected Volume Token Flow

   API Server            etcd          kubelet           Pod (container)      AWS STS
       │                  │               │                    │                 │
       │                  │               │                    │                 │
       │  [pod spec includes projected serviceAccountToken volume]              │
       │                  │               │                    │                 │
       │──WATCH event ───────────────►    │                    │                 │
       │  (pod scheduled to this node)    │                    │                 │
       │                                  │                    │                 │
       │◄── TokenRequest ─────────────────│                    │                 │
       │    POST /api/v1/namespaces/default/serviceaccounts/   │                 │
       │        payments-sa/token                              │                 │
       │    {                                                  │                 │
       │      "audiences": ["sts.amazonaws.com"],              │                 │
       │      "expirationSeconds": 3600                        │                 │
       │    }                                                  │                 │
       │                                                       │                 │
       │  [API server signs JWT with cluster's private key]    │                 │
       │  JWT payload:                                         │                 │
       │    iss: https://oidc.eks.us-east-1.amazonaws.com/id/CLUSTER_ID         │
       │    sub: system:serviceaccount:production:payments-sa  │                 │
       │    aud: sts.amazonaws.com                             │                 │
       │    exp: now + 3600                                    │                 │
       │    kubernetes.io/pod: {name, uid, namespace}          │                 │
       │                                                       │                 │
       │──► 200 {token: "eyJhbGci..."} ──────────────────────►│                 │
       │                                                       │                 │
       │         kubelet writes token to projected volume:     │                 │
       │         /var/run/secrets/kubernetes.io/serviceaccount/token            │
       │         (tmpfs mount in pod — not on disk)            │                 │
       │                                                       │                 │
       │         [kubelet schedules re-request 80% into TTL]   │                 │
       │         [~48 min for 1h token]                        │                 │
       │                                                       │                 │
       │                                                    pod starts          │
       │                                                       │                 │
       │                    ┌──── pod calls AWS API ──────────►│                 │
       │                    │     reads JWT from /var/run/...  │                 │
       │                    │                                  │                 │
       │                    │  AssumeRoleWithWebIdentity ─────────────────────► │
       │                    │  {                                                 │
       │                    │    RoleArn: arn:aws:iam::123:role/payments-sa     │
       │                    │    WebIdentityToken: <JWT>                        │
       │                    │    RoleSessionName: payments-sa                   │
       │                    │  }                                                 │
       │                    │                                                    │
       │                    │  [STS verifies JWT signature against               │
       │                    │   OIDC provider public keys from                   │
       │                    │   https://oidc.eks.../id/CLUSTER_ID/.well-known/  │
       │                    │   openid-configuration]                            │
       │                    │                                                    │
       │                    │◄── {AccessKeyId, SecretAccessKey, SessionToken} ──│
       │                    │    (valid 1 hour)                                  │
       │                    │                                                    │
       │    pod calls S3, DynamoDB, etc. with temporary credentials             │

Projected Volume Pod Spec

apiVersion: v1
kind: Pod
metadata:
  name: payments-api
  namespace: production
spec:
  serviceAccountName: payments-sa
  automountServiceAccountToken: false    # disable legacy auto-mount

  volumes:
  - name: aws-token
    projected:
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 3600        # kubelet refreshes at 80% of TTL
          audience: sts.amazonaws.com    # audience claim in JWT

  - name: kube-api-token
    projected:
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 3600
          audience: https://kubernetes.default.svc   # for calls to K8s API

  containers:
  - name: payments-api
    image: ghcr.io/acme/payments-api:latest
    volumeMounts:
    - name: aws-token
      mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      readOnly: true
    - name: kube-api-token
      mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      readOnly: true
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::123456789012:role/payments-sa
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

Token Rotation Mechanics

t=0       kubelet requests token (TTL=3600s)
t=2880s   kubelet proactively requests new token (80% of 3600s)
           → writes new token atomically to projected volume file
           → pod reads new token on next file read (no restart needed)
t=3600s   old token expires (but pod already using new token)

The file is replaced atomically (rename from temp file).
Applications must re-read the token file on each use, not cache it.

Go SDK — Correct Token Handling

// AWS SDK v2 reads AWS_WEB_IDENTITY_TOKEN_FILE automatically
// It re-reads the file on each credential refresh
cfg, err := config.LoadDefaultConfig(ctx,
    config.WithRegion("us-east-1"),
)
// No token file handling needed — SDK does it

// For Kubernetes client-go — also re-reads token file automatically
clientConfig, err := rest.InClusterConfig()
// InClusterConfig reads /var/run/secrets/kubernetes.io/serviceaccount/token
// It refreshes the token transparently

IRSA IAM Role Trust Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/CLUSTER_ID"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/CLUSTER_ID:sub":
            "system:serviceaccount:production:payments-sa",
          "oidc.eks.us-east-1.amazonaws.com/id/CLUSTER_ID:aud":
            "sts.amazonaws.com"
        }
      }
    }
  ]
}

Diagnosing Token Issues

# Verify projected token exists inside pod
kubectl exec -n production deploy/payments-api -- \
  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | \
  python3 -c "import sys,json,base64; parts=sys.stdin.read().split('.'); \
    payload=parts[1]+'=='*(-len(parts[1])%4); \
    print(json.dumps(json.loads(base64.b64decode(payload)),indent=2))"

# Expected output includes:
# "iss": "https://oidc.eks.us-east-1.amazonaws.com/id/...",
# "sub": "system:serviceaccount:production:payments-sa",
# "aud": ["sts.amazonaws.com"],
# "exp": <future unix timestamp>

# Check IRSA annotation on ServiceAccount
kubectl get sa payments-sa -n production \
  -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}'

# Manually test AssumeRoleWithWebIdentity
TOKEN=$(kubectl exec -n production deploy/payments-api -- \
  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
aws sts assume-role-with-web-identity \
  --role-arn arn:aws:iam::123456789012:role/payments-sa \
  --role-session-name test \
  --web-identity-token "$TOKEN"