Security
11 min readMay 1, 2026

Kubernetes Service Accounts and Workload Identity

Service accounts are how Kubernetes workloads authenticate to both the Kubernetes API and external systems. IRSA (IAM Roles for Service Accounts) on EKS and EKS Pod Identity allow pods to assume AWS IAM roles without static credentials. This covers the full picture: service account tokens, projected volumes, IRSA configuration with OIDC, EKS Pod Identity (the newer approach), and the security hardening that prevents token misuse.

CO
Coding Protocols Team
Platform Engineering
Kubernetes Service Accounts and Workload Identity

Service accounts are the identity mechanism for workloads running in Kubernetes. Every pod gets a service account — by default it's the default service account in its namespace, which has no RBAC permissions but still gets an auto-mounted token. That token is what the pod uses to authenticate to the Kubernetes API.

The more important use case on AWS is using service account identity to access AWS APIs without static credentials. IRSA (IAM Roles for Service Accounts) and the newer EKS Pod Identity both accomplish this — pods assume an IAM role via a projected service account token, with no long-lived AWS access keys stored anywhere.


How Service Account Tokens Work

When a pod starts, Kubernetes injects a service account token at /var/run/secrets/kubernetes.io/serviceaccount/token. This is a short-lived JWT signed by the cluster's service account key.

The token contains:

  • sub: system:serviceaccount:<namespace>:<serviceaccount-name>
  • iss: the cluster's OIDC issuer URL (for IRSA/Pod Identity)
  • exp: expiry time (default 1 hour, auto-rotated by kubelet)
  • aud: the intended audience (e.g., sts.amazonaws.com for IRSA, pods.eks.amazonaws.com for Pod Identity)

The kubelet manages the token lifecycle via the TokenRequest API — tokens are projected into pods, automatically rotated before expiry, and invalidated when the pod terminates.


Service Account Basics

yaml
1# Create a dedicated service account for the payments API
2apiVersion: v1
3kind: ServiceAccount
4metadata:
5  name: payments-api
6  namespace: payments
7  annotations:
8    # IRSA: bind this service account to an IAM role
9    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/payments-api-role
10automountServiceAccountToken: false    # Disable auto-mount; use projected volumes explicitly

Setting automountServiceAccountToken: false on the ServiceAccount prevents the token from being mounted in pods that don't need Kubernetes API access. A specific pod can override this with automountServiceAccountToken: true in its spec when it genuinely needs API access.

Assign the service account to the pod:

yaml
spec:
  serviceAccountName: payments-api
  automountServiceAccountToken: false    # Belt-and-suspenders: also disable at pod level

Projected Service Account Tokens

For IRSA and Pod Identity, you need a token with a specific audience. Use a projected volume instead of the auto-mounted token:

yaml
1spec:
2  serviceAccountName: payments-api
3  automountServiceAccountToken: false
4
5  volumes:
6    - name: aws-token
7      projected:
8        sources:
9          - serviceAccountToken:
10              audience: sts.amazonaws.com    # Required for IRSA
11              expirationSeconds: 86400       # 24h (kubelet rotates at 80% of expiry)
12              path: token
13
14  containers:
15    - name: payments-api
16      volumeMounts:
17        - name: aws-token
18          mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
19          readOnly: true
20      env:
21        - name: AWS_WEB_IDENTITY_TOKEN_FILE
22          value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
23        - name: AWS_ROLE_ARN
24          value: arn:aws:iam::123456789012:role/payments-api-role
25        - name: AWS_REGION
26          value: us-east-1

The AWS SDK (v2) automatically reads AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN to exchange the OIDC token for temporary AWS credentials via STS AssumeRoleWithWebIdentity.


IRSA: IAM Roles for Service Accounts

IRSA connects Kubernetes service account identity to AWS IAM via OIDC federation.

Setup

bash
1# 1. Get the OIDC issuer URL for your cluster
2aws eks describe-cluster --name production --query "cluster.identity.oidc.issuer" --output text
3# Output: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
4
5# 2. Create the OIDC provider in IAM (one-time per cluster)
6eksctl utils associate-iam-oidc-provider \
7  --cluster production \
8  --approve
9
10# 3. Create the IAM role with a trust policy scoped to the service account
11aws iam create-role \
12  --role-name payments-api-role \
13  --assume-role-policy-document file://trust-policy.json

Trust policy — scoped to a specific namespace and service account:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Principal": {
7        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
8      },
9      "Action": "sts:AssumeRoleWithWebIdentity",
10      "Condition": {
11        "StringEquals": {
12          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:payments:payments-api",
13          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
14        }
15      }
16    }
17  ]
18}

The StringEquals condition is critical — without it, any service account in any namespace on the cluster could assume this role.

bash
1# 4. Attach a permissions policy to the role
2aws iam attach-role-policy \
3  --role-name payments-api-role \
4  --policy-arn arn:aws:iam::123456789012:policy/payments-api-secrets-policy
5
6# 5. Annotate the service account with the role ARN
7kubectl annotate serviceaccount payments-api \
8  -n payments \
9  eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/payments-api-role

With the annotation in place, EKS automatically injects the projected token volume and AWS_WEB_IDENTITY_TOKEN_FILE / AWS_ROLE_ARN environment variables into pods using this service account.


EKS Pod Identity: The Newer Approach

EKS Pod Identity (GA since late 2023, available for EKS clusters running Kubernetes 1.24+, recommended for new clusters) simplifies IRSA by moving the trust relationship out of the IAM role and into an EKS-managed association. No OIDC provider setup, no trust policy edits.

bash
1# 1. Create the IAM role — no trust policy needed
2aws iam create-role \
3  --role-name payments-api-role \
4  --assume-role-policy-document '{
5    "Version": "2012-10-17",
6    "Statement": [{"Effect": "Allow", "Principal": {"Service": "pods.eks.amazonaws.com"}, "Action": ["sts:AssumeRole", "sts:TagSession"]}]
7  }'
8
9# 2. Attach permissions policy
10aws iam attach-role-policy \
11  --role-name payments-api-role \
12  --policy-arn arn:aws:iam::123456789012:policy/payments-api-secrets-policy
13
14# 3. Create the Pod Identity association (links the role to a k8s service account)
15aws eks create-pod-identity-association \
16  --cluster-name production \
17  --namespace payments \
18  --service-account payments-api \
19  --role-arn arn:aws:iam::123456789012:role/payments-api-role

Pod Identity requires the EKS Pod Identity Agent add-on:

bash
# Omit --addon-version to use the latest default, or pin after checking:
# aws eks describe-addon-versions --addon-name eks-pod-identity-agent
aws eks create-addon \
  --cluster-name production \
  --addon-name eks-pod-identity-agent

The Pod Identity agent runs as a DaemonSet and handles token exchange via a local HTTP endpoint (169.254.170.23) that the AWS SDK automatically queries — similar to the EC2 instance metadata service, but scoped to the pod.

IRSA vs Pod Identity

IRSAEKS Pod Identity
SetupOIDC provider + trust policy per rolePod Identity association via EKS API
Trust policyScoped to service account in trust policyAssociation stored in EKS
Cross-accountYesYes (IAM role can be in another account)
EKS versionAll EKS versionsEKS 1.24+
Agent requiredNoYes (Pod Identity Agent DaemonSet)
Recommended forExisting clustersNew clusters

For new EKS clusters, use Pod Identity — it's simpler to manage at scale (no OIDC provider, no per-role trust policy edits).


Terraform Configuration

Automate IRSA and Pod Identity setup with Terraform to avoid manual trust policy management:

hcl
1# IRSA role using the iam-role-for-service-accounts-eks module
2module "payments_api_irsa" {
3  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
4  version = "~> 5.0"
5
6  role_name = "payments-api-role"
7
8  oidc_providers = {
9    main = {
10      provider_arn               = module.eks.oidc_provider_arn
11      namespace_service_accounts = ["payments:payments-api"]
12    }
13  }
14
15  role_policy_arns = {
16    payments = aws_iam_policy.payments_api.arn
17  }
18}
19
20# For Pod Identity (simpler — no OIDC provider ARN needed):
21resource "aws_eks_pod_identity_association" "payments_api" {
22  cluster_name    = module.eks.cluster_name
23  namespace       = "payments"
24  service_account = "payments-api"
25  role_arn        = aws_iam_role.payments_api.arn
26}

Cross-Account IAM with Pod Identity

In multi-account AWS organizations, EKS Pod Identity simplifies cross-account access. You no longer need to configure OIDC trust across accounts. Instead, a service account in the EKS Account can assume a role in a Resource Account by following these steps:

  1. Resource Account: Create the IAM role with a trust policy that allows the EKS account to assume it.
  2. EKS Account: Create a "bridge role" that pods assume via Pod Identity.
  3. Bridge Role: Grant the bridge role permission to call sts:AssumeRole on the role in the Resource Account.

This architecture centralizes identity management while maintaining strict account boundaries for resources like RDS and S3.


Verifying IRSA and Pod Identity

bash
1# Check the service account annotation (IRSA)
2kubectl get serviceaccount payments-api -n payments -o yaml | grep role-arn
3
4# Check Pod Identity associations (filter by namespace; pipe through jq to filter by service account)
5aws eks list-pod-identity-associations \
6  --cluster-name production \
7  --namespace payments
8
9# Verify credentials work inside a pod
10kubectl exec -n payments deployment/payments-api -- \
11  aws sts get-caller-identity
12# Should show: arn:aws:iam::123456789012:assumed-role/payments-api-role/...
13
14# Check projected token exists and has correct audience
15kubectl exec -n payments deployment/payments-api -- \
16  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | \
17  python3 -c "import sys,base64,json; p=sys.stdin.read().split('.')[1]; print(json.dumps(json.loads(base64.b64decode(p+'==').decode()), indent=2))"

Security Hardening

Token Audience Restrictions

By default, auto-mounted tokens have aud: ["https://kubernetes.default.svc"]. IRSA tokens have aud: ["sts.amazonaws.com"]. A token can only be used for its declared audience — an IRSA token cannot authenticate to the Kubernetes API, and a Kubernetes API token cannot call AWS STS.

For workloads that need both Kubernetes API access and AWS access, mount two separate projected tokens with different audiences:

yaml
1volumes:
2  - name: kube-token
3    projected:
4      sources:
5        - serviceAccountToken:
6            audience: https://kubernetes.default.svc
7            expirationSeconds: 3600
8            path: token
9  - name: aws-token
10    projected:
11      sources:
12        - serviceAccountToken:
13            audience: sts.amazonaws.com
14            expirationSeconds: 86400
15            path: token

Prevent Cross-Namespace Token Use

Service account tokens are namespace-scoped but can be used from any namespace if stolen. Defense in depth:

  • NetworkPolicy: block pod-to-pod traffic across namespaces (limits lateral movement)
  • Short token expiry (1h default, rotate before use)
  • Audit logging on secrets and serviceaccounts resources
  • Never share service accounts across namespaces

Disable Default Service Account Auto-Mount

Apply organization-wide: patch the default service account in every namespace to prevent auto-mounting:

bash
# Apply to all namespaces
kubectl get namespaces -o name | xargs -I{} bash -c \
  'ns=${1##namespace/}; kubectl patch serviceaccount default -n $ns -p '"'"'{"automountServiceAccountToken":false}'"'"'' \
  -- {}

Or use a Kyverno policy to enforce automountServiceAccountToken: false on all new ServiceAccounts that don't explicitly opt in.


Debugging Credential Failures

When IRSA or Pod Identity stops working, these seven failure types cover the vast majority of incidents:

bash
1# 1. Missing annotation or association — check SA annotation (IRSA) or Pod Identity association
2kubectl describe sa payments-api -n payments
3
4# 2. Token file not injected — check that the projected token file exists
5kubectl exec -n payments payments-api-xxx -- \
6  ls /var/run/secrets/eks.amazonaws.com/serviceaccount/
7
8# 3. STS unreachable — check endpoint connectivity from the pod
9kubectl exec -n payments payments-api-xxx -- \
10  curl -s https://sts.amazonaws.com/
11
12# 4. Wrong resolved identity — verify STS resolves to the expected role
13kubectl exec -n payments payments-api-xxx -- \
14  aws sts get-caller-identity
15
16# 5. Pod Identity agent not running — check agent logs
17kubectl logs -n kube-system -l app.kubernetes.io/name=eks-pod-identity-agent
18
19# 6. Trust policy mismatch (IRSA) — verify OIDC condition matches SA namespace/name
20aws iam get-role --role-name payments-api-role \
21  --query 'Role.AssumeRolePolicyDocument'
22
23# 7. Cached expired credentials — check STS token expiry; ensure AWS SDK v2+ is used
24#    AWS SDK v2/v3 automatically re-reads token file; SDK v1 may cache past expiry

Common error messages and their root causes:

  • AccessDenied: Not authorized to assume role — trust policy condition doesn't match the ServiceAccount namespace/name, or OIDC provider URL in trust policy has a typo
  • WebIdentityErr: Failed to retrieve credentials — token file not present (webhook not running, or automountServiceAccountToken: false without explicit projected volume)
  • ExpiredToken — application cached STS credentials and didn't refresh. Use the AWS SDK credential provider chain (not a cached static credential object) — the SDK automatically refreshes before expiry

Frequently Asked Questions

Should I use one IAM role per service account or one role for multiple service accounts?

One role per service account. The principle of least privilege: if the payments-api role is shared with payments-worker, a compromise of either workload grants access to both sets of AWS permissions. Roles are free in IAM; the overhead of creating one per service account is minimal compared to the blast radius reduction.

What happens if the projected token expires before rotation?

The kubelet automatically rotates projected tokens at 80% of their expiry (e.g., for a 24h token, rotation happens at 19.2h). Applications using the AWS SDK v2 or v3 re-read the token file automatically on each STS call — no restart required. Older applications (AWS SDK v1) may need a restart after rotation if they cache credentials past token expiry.

Can I use IRSA or Pod Identity outside EKS?

IRSA's OIDC mechanism works with any Kubernetes cluster that has an OIDC issuer. Self-managed clusters on EC2 can use IRSA if you register the cluster's OIDC endpoint as an IAM identity provider. GKE Workload Identity and AKS Workload Identity follow the same OIDC federation pattern but integrate with their respective cloud IAM systems. EKS Pod Identity is EKS-specific.


For the RBAC permissions that control what service accounts can do within Kubernetes, see Kubernetes RBAC Advanced Patterns. For ESO using service account IRSA to pull secrets from AWS Secrets Manager, see External Secrets Operator: Syncing AWS, GCP, and Vault Secrets to Kubernetes.

Migrating from hardcoded AWS credentials in pods to IRSA or Pod Identity? Talk to us at Coding Protocols — we help platform teams implement workload identity that eliminates static credentials without disrupting running services.

Related Topics

Kubernetes
Service Accounts
IRSA
EKS
IAM
Workload Identity
Security
OIDC
Platform Engineering

Read Next