This blog caters to software engineers working on Infrastructure Platform teams and trying to build Internal Developer Platforms, manage existing infrastructure platforms etc..

Service account tokens are the cornerstone of pod authentication in Kubernetes. With the introduction of projected service account tokens, Kubernetes has significantly improved security and flexibility in how pods authenticate to the API server and external services.

What Are Projected Service Account Tokens?

Projected service account tokens are time-bound, audience-scoped JSON Web Tokens (JWTs) that replace the legacy non-expiring service account tokens. They provide enhanced security through:

  • Time-bound expiration: Tokens automatically expire and are rotated
  • Audience binding: Tokens can be scoped to specific audiences
  • Automatic rotation: The kubelet automatically refreshes tokens before expiration

The Problem with Legacy Service Account Tokens

Before projected tokens, Kubernetes used legacy service account tokens that had several security limitations:

  • Never expire: Once created, they remain valid indefinitely unless manually revoked
  • No audience restriction: Can be used to authenticate to any service that accepts them
  • Stored as Secrets: Persisted in etcd, increasing the attack surface
  • Broad scope: If compromised, provide unrestricted access to the API server
  • Manual rotation: Required manual intervention to refresh or rotate

These limitations meant that if a token was leaked or a pod was compromised, attackers could potentially maintain persistent access to your cluster. Projected tokens solve these problems by being short-lived, automatically rotated, and scoped to specific audiences.

How Projected Tokens Work

Understanding the TokenRequest API

The TokenRequest API is a Kubernetes API (not provided by cloud providers) that generates service account tokens on-demand. It’s part of the core Kubernetes API server and was introduced in Kubernetes 1.12 (stable in 1.20).

Key characteristics:

  • Endpoint: /api/v1/namespaces/{namespace}/serviceaccounts/{name}/token
  • Purpose: Creates short-lived, audience-bound tokens for service accounts
  • Parameters: Accepts expiration time and audience claims
  • Signature: Tokens are signed by the Kubernetes API server’s private key

When you use a projected volume, the kubelet automatically calls this API on your behalf to request tokens, eliminating the need for manual token management.

What is a Projected Volume?

A projected volume is a special volume type in Kubernetes that can project (combine) multiple volume sources into a single directory. Think of it as a way to mount different types of data into your pod from various sources.

Common sources that can be projected:

  • serviceAccountToken: Dynamically generated tokens via TokenRequest API
  • configMap: Configuration data
  • secret: Sensitive data
  • downwardAPI: Pod metadata

For service account tokens, projected volumes enable the kubelet to:

  1. Request fresh tokens from the TokenRequest API
  2. Automatically refresh tokens before expiration
  3. Mount tokens as files in the pod’s filesystem
  4. Handle all the complexity of token lifecycle management

This is different from the legacy approach where tokens were stored as static Secrets and mounted directly.

Token Generation Flow

Projected tokens use the TokenRequest API to generate short-lived tokens on-demand. Here’s the typical flow:

Basic Configuration

Here’s a simple example of configuring a projected service account token:

apiVersion: v1
kind: Pod
metadata:
name: token-demo
spec:
serviceAccountName: my-service-account
containers:
- name: app
image: nginx
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: my-app

Using Projected Tokens with AKS (Azure Kubernetes Service)

AKS leverages projected tokens for Workload Identity, enabling pods to authenticate to Azure services without storing credentials.

Azure-Side Configuration

Before using Workload Identity in AKS, you need to set up the Azure side:

# 1. Create an Azure AD application (or Managed Identity)
az ad sp create-for-rbac --name "myapp-workload-identity"
# 2. Get the application's client ID
export APPLICATION_CLIENT_ID="<your-client-id>"
# 3. Create federated identity credential that trusts your AKS cluster
az ad app federated-credential create \
--id $APPLICATION_CLIENT_ID \
--parameters '{
"name": "myapp-federated-credential",
"issuer": "https://oidc.prod-aks.azure.com/<tenant-id>/<cluster-oidc-issuer-id>/",
"subject": "system:serviceaccount:default:workload-identity-sa",
"audiences": ["api://AzureADTokenExchange"]
}'
# 4. Assign Azure RBAC roles to the application
az role assignment create \
--assignee $APPLICATION_CLIENT_ID \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.Storage/storageAccounts/<storage-account>"

Key Configuration Points:

  • Issuer: Your AKS cluster’s OIDC issuer URL (unique per cluster)
  • Subject: Must match the format system:serviceaccount:<namespace>:<service-account-name>
  • Audiences: Must be api://AzureADTokenExchange for Workload Identity

AKS Workload Identity Setup

apiVersion: v1
kind: ServiceAccount
metadata:
name: workload-identity-sa
namespace: default
annotations:
azure.workload.identity/client-id: "YOUR_AZURE_CLIENT_ID"
---
apiVersion: v1
kind: Pod
metadata:
name: aks-workload-identity-demo
namespace: default
labels:
azure.workload.identity/use: "true" # This label triggers the webhook to inject volumes
spec:
serviceAccountName: workload-identity-sa
containers:
- name: app
image: mcr.microsoft.com/azure-cli
command: ["sleep", "infinity"]
# Note: The following are automatically injected by the AKS Workload Identity webhook
# when the pod has the label "azure.workload.identity/use: true":
#
# Environment variables:
# - AZURE_CLIENT_ID
# - AZURE_TENANT_ID
# - AZURE_FEDERATED_TOKEN_FILE
# - AZURE_AUTHORITY_HOST
#
# Volume mounts:
# - name: azure-identity-token
# mountPath: /var/run/secrets/azure/tokens
# readOnly: true
#
# Volumes:
# - name: azure-identity-token
# projected:
# sources:
# - serviceAccountToken:
# path: azure-identity-token
# expirationSeconds: 3600
# audience: api://AzureADTokenExchange

Important: In practice, when using AKS Workload Identity, you typically only need to:

  1. Annotate your service account with azure.workload.identity/client-id
  2. Add the label azure.workload.identity/use: "true" to your pod
  3. Reference that service account in your pod spec

The pod spec would look like this:

apiVersion: v1
kind: Pod
metadata:
name: aks-workload-identity-demo
namespace: default
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: workload-identity-sa
containers:
- name: app
image: mcr.microsoft.com/azure-cli
command: ["sleep", "infinity"]
# Everything else is auto-injected!

AKS will automatically inject the environment variables, volume mounts, and projected volumes for you through its mutating admission webhook.

How it works in AKS:

Using Projected Tokens with EKS (Elastic Kubernetes Service)

EKS uses projected tokens for IAM Roles for Service Accounts (IRSA), allowing pods to assume AWS IAM roles.

AWS-Side Configuration

Before using IRSA in EKS, you need to configure AWS IAM:

# 1. Get your EKS cluster's OIDC provider URL
aws eks describe-cluster --name my-cluster --query "cluster.identity.oidc.issuer" --output text
# Output: https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
# 2. Create an IAM OIDC identity provider for your cluster
# Note: If you created your cluster with eksctl or with OIDC enabled, this may already exist
# You can verify with: aws iam list-open-id-connect-providers
eksctl utils associate-iam-oidc-provider --cluster my-cluster --approve
# 3. Create an IAM policy for S3 access
cat > s3-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
EOF
aws iam create-policy --policy-name S3AccessPolicy --policy-document file://s3-policy.json
# 4. Create an IAM role with a trust policy that allows the service account
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:default:s3-access-sa",
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF
aws iam create-role --role-name s3-access-role --assume-role-policy-document file://trust-policy.json
# 5. Attach the policy to the role
aws iam attach-role-policy \
--role-name s3-access-role \
--policy-arn arn:aws:iam::ACCOUNT_ID:policy/S3AccessPolicy

Key Configuration Points:

  • Trust Policy Condition: Must match system:serviceaccount:<namespace>:<service-account-name>
  • Audience: Must be sts.amazonaws.com for IRSA
  • OIDC Provider: Must be registered as a trusted identity provider in IAM

EKS IRSA Configuration

apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-access-sa
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/s3-access-role
---
apiVersion: v1
kind: Pod
metadata:
name: eks-irsa-demo
namespace: default
spec:
serviceAccountName: s3-access-sa
containers:
- name: app
image: amazon/aws-cli
command: ["sleep", "infinity"]
# Note: The following are automatically injected by the EKS Pod Identity Webhook
# when the service account has the annotation "eks.amazonaws.com/role-arn":
#
# Environment variables:
# - AWS_ROLE_ARN: arn:aws:iam::ACCOUNT_ID:role/s3-access-role
# - AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
#
# Volume mounts:
# - name: aws-iam-token
# mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
# readOnly: true
#
# Volumes:
# - name: aws-iam-token
# projected:
# sources:
# - serviceAccountToken:
# path: token
# expirationSeconds: 86400
# audience: sts.amazonaws.com

Important: In practice, when using EKS with IRSA, you typically only need to:

  1. Annotate your service account with eks.amazonaws.com/role-arn
  2. Reference that service account in your pod spec

The pod spec would look like this:

apiVersion: v1
kind: Pod
metadata:
name: eks-irsa-demo
namespace: default
spec:
serviceAccountName: s3-access-sa
containers:
- name: app
image: amazon/aws-cli
command: ["sleep", "infinity"]
# Everything else is auto-injected!

EKS will automatically inject the environment variables, volume mounts, and projected volumes for you. The full configuration above is shown to illustrate what happens behind the scenes.

How it works in EKS:

Using Projected Tokens with GKE (Google Kubernetes Engine)

GKE uses projected tokens for Workload Identity, enabling pods to authenticate as Google Cloud service accounts.

GCP-Side Configuration

Before using Workload Identity in GKE, you need to configure Google Cloud:

# 1. Enable Workload Identity on your GKE cluster (if not already enabled)
gcloud container clusters update my-cluster \
--workload-pool=PROJECT_ID.svc.id.goog
# 2. Create a Google Cloud service account
gcloud iam service-accounts create gcs-access-sa \
--display-name="GCS Access Service Account"
# 3. Grant the GCP service account permissions to Cloud resources
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:gcs-access-sa@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# 4. Create the IAM policy binding between the Kubernetes SA and GCP SA
gcloud iam service-accounts add-iam-policy-binding \
gcs-access-sa@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[default/gke-workload-identity-sa]"

Key Configuration Points:

  • Workload Identity Pool: Format is PROJECT_ID.svc.id.goog
  • Member Binding: Must match serviceAccount:PROJECT_ID.svc.id.goog[<namespace>/<ksa-name>]
  • Role: The GCP service account needs roles/iam.workloadIdentityUser for the K8s SA

The member format breaks down as:

  • PROJECT_ID.svc.id.goog – Your workload identity pool
  • [default/gke-workload-identity-sa][namespace/kubernetes-service-account]

GKE Workload Identity Setup

apiVersion: v1
kind: ServiceAccount
metadata:
name: gke-workload-identity-sa
namespace: default
annotations:
iam.gke.io/gcp-service-account: my-gsa@PROJECT_ID.iam.gserviceaccount.com
---
apiVersion: v1
kind: Pod
metadata:
name: gke-workload-identity-demo
namespace: default
spec:
serviceAccountName: gke-workload-identity-sa
containers:
- name: app
image: google/cloud-sdk:slim
command: ["sleep", "infinity"]
# Note: GKE Workload Identity automatically configures the GCP metadata server
# in the pod. Application Default Credentials (ADC) will automatically work
# without needing explicit volume mounts or environment variables.

How it works in GKE:

Note on GKE and Projected Volumes: Unlike AKS and EKS, GKE’s Workload Identity primarily works through metadata server emulation. You can optionally use projected service account tokens with a specific audience if you need direct access to the Kubernetes token, but this is rarely necessary. Most applications using Google Cloud client libraries will authenticate automatically through the metadata server without any explicit volume configuration.

Cloud Provider Comparison

Trust Relationship Overview

All three cloud providers use a similar pattern: establishing trust between the Kubernetes service account and cloud provider IAM system through OIDC federation.

Provider-Specific Comparison

FeatureAKSEKSGKE
Trust MechanismFederated Identity CredentialIAM OIDC Provider + Trust PolicyWorkload Identity Pool Binding
Subject Formatsystem:serviceaccount:ns:sasystem:serviceaccount:ns:saserviceAccount:PROJECT.svc.id.goog[ns/sa]
Audienceapi://AzureADTokenExchangests.amazonaws.comhttps://iam.googleapis.com/...
K8s Annotationazure.workload.identity/client-ideks.amazonaws.com/role-arniam.gke.io/gcp-service-account
Pod Label Requiredazure.workload.identity/use: "true"NoNo
Auto-InjectionYes (via webhook)Yes (via webhook)Yes (metadata server)
Env Variables InjectedAZURE_CLIENT_ID, AZURE_TENANT_ID, etc.AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILENone (uses metadata server)
Volume Auto-MountYesYesTypically not needed
Cloud IAM SetupFederated credential on App/MIIAM Role with trust policyIAM binding with workloadIdentityUser

Key Benefits Across All Platforms

  1. No Long-Lived Credentials: Tokens expire automatically, reducing security risk
  2. Automatic Rotation: The kubelet handles token refresh transparently
  3. Fine-Grained Access: Audience scoping limits token usage
  4. Cloud Integration: Seamless authentication to cloud provider services
  5. Least Privilege: Each pod gets only the permissions it needs

Best Practices

  • Set appropriate expiration times: Balance between security (shorter) and performance (fewer rotations)
  • Use specific audiences: Scope tokens to their intended use
  • Monitor token usage: Track authentication patterns for security insights
  • Follow cloud provider guides: Each platform has specific setup requirements
  • Test token rotation: Ensure your applications handle token refresh gracefully

Conclusion

Projected service account tokens represent a significant security improvement in Kubernetes authentication. Whether you’re running on AKS, EKS, or GKE, understanding how these tokens work enables you to build secure, cloud-native applications that follow the principle of least privilege without managing long-lived credentials.

The integration with cloud provider IAM systems makes projected tokens essential for modern Kubernetes workloads, providing a secure bridge between your containerized applications and cloud services.

Leave a comment