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.

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.comfor IRSA,pods.eks.amazonaws.comfor 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
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 explicitlySetting 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:
spec:
serviceAccountName: payments-api
automountServiceAccountToken: false # Belt-and-suspenders: also disable at pod levelProjected 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:
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-1The 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
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.jsonTrust policy — scoped to a specific namespace and service account:
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.
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-roleWith 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.
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-rolePod Identity requires the EKS Pod Identity Agent add-on:
# 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-agentThe 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
| IRSA | EKS Pod Identity | |
|---|---|---|
| Setup | OIDC provider + trust policy per role | Pod Identity association via EKS API |
| Trust policy | Scoped to service account in trust policy | Association stored in EKS |
| Cross-account | Yes | Yes (IAM role can be in another account) |
| EKS version | All EKS versions | EKS 1.24+ |
| Agent required | No | Yes (Pod Identity Agent DaemonSet) |
| Recommended for | Existing clusters | New 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:
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:
- Resource Account: Create the IAM role with a trust policy that allows the EKS account to assume it.
- EKS Account: Create a "bridge role" that pods assume via Pod Identity.
- Bridge Role: Grant the bridge role permission to call
sts:AssumeRoleon 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
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:
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: tokenPrevent 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
secretsandserviceaccountsresources - 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:
# 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:
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 expiryCommon 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 typoWebIdentityErr: Failed to retrieve credentials— token file not present (webhook not running, orautomountServiceAccountToken: falsewithout 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.


