EKS Pod Identity and IRSA
In one line
Section titled “In one line”A pod proves who it is with a Kubernetes service account token, and AWS exchanges that token for temporary credentials of one IAM role. With IRSA the pod’s SDK makes the exchange itself at STS through OIDC federation. With EKS Pod Identity an agent on the node makes it through the EKS Auth API.
Why it exists
Section titled “Why it exists”Every AWS API call must be signed with credentials. The question is where a container gets them.
- Node instance role. An EC2 instance gets credentials for its instance profile from the Instance Metadata Service (IMDS) at
169.254.169.254. Every pod on the node can reach that address, so every pod has the node’s permissions. Give one application access to an S3 bucket and every other workload on that node has it too. A compromised pod also inherits what the kubelet and the CNI plugin can do. - Static access keys in a Secret. Scoped per application, but long-lived, copied around, and never rotated in practice.
- kube2iam and kiam. Community tools that ran an agent on each node and redirected pod traffic for
169.254.169.254to it. The agent looked up the pod by source IP, read a role name from a pod annotation, assumed that role and returned its credentials. It worked, but the node (or a set of server nodes in kiam) had to be allowed to assume every application role, the IP-to-pod lookup could race at pod start, and the interception was one more thing to break. - IRSA, IAM Roles for Service Accounts (September 2019). AWS made the pod a first-class IAM identity. Kubernetes 1.12 had added projected service account tokens: OIDC-compliant JWTs with a chosen audience and an expiry. IAM had supported OIDC federation since 2014. IRSA connects the two: the cluster is an OIDC issuer, IAM trusts it, and the SDK swaps the token for role credentials. No agent, no interception.
- EKS Pod Identity (November 2023). IRSA scales badly in the IAM dimension: one IAM OIDC provider per cluster, and every role’s trust policy names specific clusters. Pod Identity moves the mapping into an EKS API (a pod identity association) and uses one fixed service principal for all clusters, so a role works in a new cluster without touching IAM. It also adds session tags, which IRSA cannot.
AWS recommends EKS Pod Identity where it is available. IRSA remains the answer everywhere Pod Identity does not run.
How it works
Section titled “How it works”Both mechanisms start from the same primitive: a projected service account token. The kubelet asks the API server for a JWT bound to the pod, with a specific audience (aud) and expiry, writes it to a file in the pod and replaces it before it expires. The token’s sub is system:serviceaccount:NAMESPACE:SERVICE_ACCOUNT.
IRSA step by step
Section titled “IRSA step by step”Setup, once per cluster:
- Cluster OIDC issuer. Every EKS cluster has an issuer URL,
https://oidc.eks.REGION.amazonaws.com/id/CLUSTER_ID. EKS hosts a public OIDC discovery endpoint there with the keys that sign service account tokens. The signing key rotates every 7 days. - IAM OIDC provider. You register that issuer URL in IAM as an identity provider with the audience
sts.amazonaws.com. This is what tells IAM to accept tokens from this cluster.
Setup, once per application:
-
Role trust policy. The role trusts the OIDC provider and restricts which service account may use it:
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" },"Action": "sts:AssumeRoleWithWebIdentity","Condition": {"StringEquals": {"oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:payments:api","oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"}}}]}Without the
subcondition, any service account in the cluster can assume the role. -
ServiceAccount annotation.
apiVersion: v1kind: ServiceAccountmetadata:name: apinamespace: paymentsannotations:eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/payments-api
At run time:
-
Mutating webhook. When a pod that uses this service account is created, the EKS pod identity webhook (it runs in the managed control plane) changes the pod spec. It adds a projected token volume with audience
sts.amazonaws.comandexpirationSeconds: 86400, and these variables:AWS_ROLE_ARN=arn:aws:iam::111122223333:role/payments-apiAWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/tokenOptional annotations add
AWS_STS_REGIONAL_ENDPOINTS=regional(eks.amazonaws.com/sts-regional-endpoints) and change the expiry (eks.amazonaws.com/token-expiration). The webhook acts only at pod creation, so pods that existed before the annotation need a restart. -
SDK. The AWS SDK’s default credential chain finds the two variables, reads the token file and calls
sts:AssumeRoleWithWebIdentitywith the token and the role ARN. The call is not signed. The token is the proof. -
STS validates. It finds the IAM OIDC provider that matches the token’s
iss, checks the signature against the issuer’s published keys, checksaudand expiry, then evaluates the role trust policy conditions onsubandaud. -
Credentials. STS returns an access key, secret key and session token (1 hour unless a longer duration is requested). The SDK caches them and repeats the exchange before they expire.
EKS Pod Identity step by step
Section titled “EKS Pod Identity step by step”Setup, once per cluster:
- Agent. Install the
eks-pod-identity-agentadd-on. It is a DaemonSet inkube-systemthat uses the node’shostNetworkand listens on the link-local address169.254.170.23(IPv4) and[fd00:ec2::23](IPv6), ports 80 and 2703. EKS Auto Mode has it built in. The node role must alloweks-auth:AssumeRoleForPodIdentity, which the managed policyAmazonEKSWorkerNodePolicyincludes. Nodes in private subnets need an interface endpoint for the EKS Auth API.
Setup, once per application:
-
Role trust policy. The same statement for every cluster:
{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": { "Service": "pods.eks.amazonaws.com" },"Action": ["sts:AssumeRole", "sts:TagSession"]}]}sts:TagSessionis required because EKS attaches session tags. To pin the role to one cluster, namespace or service account, add conditions on the request tags, for exampleaws:RequestTag/kubernetes-namespace. -
Pod identity association. An EKS API object that maps (cluster, namespace, service account) to a role. Nothing changes inside the cluster: no annotation, no Kubernetes object.
Terminal window aws eks create-pod-identity-association \--cluster-name prod-eu --namespace payments --service-account api \--role-arn arn:aws:iam::111122223333:role/payments-api
At run time:
-
Pod mutation. When a pod with an associated service account is created, EKS adds a projected token with audience
pods.eks.amazonaws.com(expirationSeconds: 86400) and:AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentialsAWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token -
SDK. The container credential provider in the default chain (the same one ECS tasks use) sends an HTTP GET to that URI with the token in the
Authorizationheader. -
Agent. The agent on the same node passes the token to the EKS Auth API action
AssumeRoleForPodIdentity. -
EKS Auth. It validates the token, looks up the association, and assumes the role with
sts:AssumeRoleplussts:TagSession, attaching session tags. Because a service principal makes that call, it does not count against your account’s STS request quota. -
Credentials. The agent returns them to the SDK and caches them: 6 hours for a plain association, 59 minutes when the association has a target role.
Session tags attached to every Pod Identity session: eks-cluster-arn, eks-cluster-name, kubernetes-namespace, kubernetes-service-account, kubernetes-pod-name, kubernetes-pod-uid. They enable attribute-based access control (ABAC): one role and one policy for many service accounts, with access decided by matching tags.
{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::tenant-data/${aws:PrincipalTag/kubernetes-namespace}/*"}The tags are transitive, so they survive a further AssumeRole into another account. You cannot add custom session tags. If the packed session gets too large (PackedPolicyTooLarge), session tags can be disabled per association.
Cross-account access
Section titled “Cross-account access”| Mechanism | How | Notes |
|---|---|---|
| IRSA, option 1 | the other account creates its own IAM OIDC provider for your cluster’s issuer, and a role that trusts it. You annotate the service account with that role’s ARN | one hop, but the other account manages an OIDC provider per cluster |
| IRSA, option 2 | role chaining: the pod gets a role in the cluster’s account through IRSA, then calls sts:AssumeRole on the role in the other account |
the application or its AWS profile must do the second hop |
| Pod Identity | the association takes a second role, the target role, in the other account. EKS does the chaining: Pod Identity role first, then the target role | the first role must be in the cluster’s account. Session tags pass through. Chained credentials are cached 59 minutes |
Compare
Section titled “Compare”Ways to give a pod AWS credentials
Section titled “Ways to give a pod AWS credentials”| Node instance role | kube2iam, kiam | IRSA | EKS Pod Identity | |
|---|---|---|---|---|
| Year | from the start | community, before 2019 | September 2019 | November 2023 |
| Granularity | per node: every pod shares it | per pod, by annotation | per service account | per service account |
| How the pod gets credentials | IMDS at 169.254.169.254 |
agent intercepts IMDS calls and assumes a role for the pod | SDK exchanges a service account token at STS | SDK asks the node agent, which calls the EKS Auth API |
| What you install | nothing | DaemonSet (plus servers for kiam), traffic redirection | nothing on EKS. The webhook runs in the control plane | eks-pod-identity-agent add-on (built into Auto Mode) |
| Per-cluster IAM setup | instance profile | node or server role allowed to assume all pod roles | one IAM OIDC provider per cluster | none |
| Role trust policy | ec2.amazonaws.com |
the node or server role | federated OIDC provider, conditions on sub and aud, per cluster |
pods.eks.amazonaws.com, sts:AssumeRole + sts:TagSession, same for all clusters |
| Mapping lives in | launch template | pod annotation | ServiceAccount annotation (Kubernetes) | pod identity association (EKS API) |
| Reuse a role in a new cluster | attach the profile | allow the new cluster’s nodes | edit the trust policy to add the new issuer | no IAM change. Create the association |
| Limits | none specific | none specific | 100 OIDC providers per account by default (quota up to 700). Trust policy 2,048 characters by default (quota up to 8,192), about 4 trust relationships at the default size | 5,000 associations per cluster |
| Session tags, ABAC | no | no | no | yes: cluster, namespace, service account, pod name and UID |
| STS calls | by EC2, not your quota | by the agent, your quota | by each pod, your STS quota (throttling is possible) | by the EKS service, not your quota |
| Cross-account | AssumeRole from the node role |
assume a role in the other account | OIDC provider in the other account, or role chaining | target role on the association |
| SDK requirement | any | any (it looks like IMDS) | web identity token support, for example boto3 1.9.220, Java v2 2.10.11, Go v1 1.23.13 | container credentials with token file, for example boto3 1.34.41, Java v2 2.21.30, Go v1 1.47.11, AWS CLI 2.15.0 |
| Isolation between pods | none | depends on the interception holding | strong when pod access to IMDS is blocked | strong when pod access to IMDS is blocked |
| Audit trail in CloudTrail | the node role and instance ID | assumed role sessions made by the agent | AssumeRoleWithWebIdentity with the token subject |
AssumeRoleForPodIdentity in EKS Auth, sessions carry the tags |
| Use it when | node-level agents only (kubelet, CNI, CSI that need the node) | legacy clusters, plan a migration | Fargate, Windows, EKS Anywhere, self-managed clusters, non-EKS | EKS on Linux EC2 nodes: the default choice |
Where each one works
Section titled “Where each one works”| Environment | IRSA | EKS Pod Identity |
|---|---|---|
| EKS, Linux EC2 nodes | yes | yes |
| EKS Auto Mode | yes | yes, agent built in |
| EKS on Fargate (Linux and Windows pods) | yes | no |
| EKS, Windows EC2 nodes | yes | no |
| EKS on AWS Outposts | not stated in the comparison, check the Outposts guide | no |
| EKS Anywhere | yes | no |
| Red Hat OpenShift Service on AWS | yes | no |
| Self-managed Kubernetes on EC2 | yes, you host the issuer keys and run the webhook | no |
| Minimum cluster version | all supported EKS versions | all supported EKS versions (1.28 needs platform version eks.4 or later) |
The support matrix changes. Check the restrictions list in the EKS user guide before relying on a row.
The same idea on other clouds
Section titled “The same idea on other clouds”All of them use the same building block: a projected service account token from a cluster that acts as an OIDC issuer, exchanged for a cloud token.
| EKS Pod Identity | IRSA | GKE Workload Identity Federation | AKS with Microsoft Entra Workload ID | |
|---|---|---|---|---|
| Trust anchor | EKS service principal pods.eks.amazonaws.com |
IAM OIDC provider per cluster | one workload identity pool per project, PROJECT_ID.svc.id.goog, clusters register as providers |
cluster OIDC issuer, plus a federated identity credential on a managed identity or app registration |
| Who exchanges the token | node agent, through the EKS Auth API | the SDK in the pod, at STS | the GKE metadata server on the node, at Google’s Security Token Service | the SDK in the pod (Azure Identity or MSAL), at Microsoft Entra ID |
| How the pod is wired | env vars and token injected by EKS | env vars and token injected by a webhook | the pod calls the metadata address as on any VM. The GKE metadata server (a DaemonSet) intercepts it | webhook injects AZURE_FEDERATED_TOKEN_FILE and related variables into pods labelled azure.workload.identity/use: "true" |
| Binding | pod identity association | ServiceAccount annotation eks.amazonaws.com/role-arn + trust policy |
IAM policy on the resource for a principal such as principal://iam.googleapis.com/projects/NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NS/sa/SA, or impersonation of an IAM service account |
ServiceAccount annotation azure.workload.identity/client-id + federated credential naming issuer and subject |
| Token audience | pods.eks.amazonaws.com |
sts.amazonaws.com |
handled by the metadata server | api://AzureADTokenExchange |
| Worth knowing | 5,000 associations per cluster, EKS only | OIDC provider and trust policy limits | identity sameness: same namespace and service account name in two clusters of one project is one identity. hostNetwork pods are not covered |
at most 20 federated identity credentials per managed identity |
What breaks
Section titled “What breaks”Start inside the pod. Three commands tell you which mechanism is active and which identity you really have.
# 1. Who am Iaws sts get-caller-identity# arn:aws:sts::111122223333:assumed-role/payments-api/... the pod role: good# arn:aws:sts::111122223333:assumed-role/eks-node-role/i-0abc... the node role: the pod fell back to IMDS
# 2. Which mechanism was injectedenv | grep -E '^AWS_'# IRSA: AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE# Pod Identity: AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
# 3. What the token says (iss, aud, sub, exp)cut -d. -f2 "$AWS_WEB_IDENTITY_TOKEN_FILE" | base64 -d 2>/dev/null; echo| Symptom | Cause | Check |
|---|---|---|
get-caller-identity shows the node role |
no injection happened, or the SDK is too old to use it, and the pod can reach IMDS | env; SDK version; kubectl get pod POD -o yaml for the projected volume |
No AWS_* variables in the pod (IRSA) |
the pod was created before the annotation, the annotation key is misspelt, or the pod uses another service account | kubectl get sa api -n payments -o yaml; kubectl get pod POD -o jsonpath='{.spec.serviceAccountName}'; restart the pod |
No AWS_* variables in the pod (Pod Identity) |
no association for this namespace and service account, or the pod predates it | aws eks list-pod-identity-associations --cluster-name prod-eu --namespace payments |
AccessDenied ... not authorized to perform sts:AssumeRoleWithWebIdentity |
trust policy mismatch: wrong namespace or name in sub, wrong issuer ID, aud condition, or StringEquals on a wildcard that needs StringLike |
compare the token’s iss and sub with aws iam get-role --role-name payments-api --query Role.AssumeRolePolicyDocument |
InvalidIdentityToken ... No OpenIDConnect provider found |
the IAM OIDC provider for this cluster’s issuer does not exist in the role’s account | aws eks describe-cluster --name prod-eu --query cluster.identity.oidc.issuer; aws iam list-open-id-connect-providers |
| Worked in cluster A, denied in cluster B (IRSA) | the role’s trust policy names only cluster A’s issuer | add a statement for B’s issuer, or move to Pod Identity |
Pod Identity: SDK times out or connection refused to 169.254.170.23 |
agent not running on that node, or an HTTP proxy variable sends the request away | kubectl get ds eks-pod-identity-agent -n kube-system; add 169.254.170.23 and [fd00:ec2::23] to NO_PROXY |
| Pod Identity: agent logs access denied | node role lacks eks-auth:AssumeRoleForPodIdentity, or the role’s trust policy lacks sts:TagSession or the pods.eks.amazonaws.com principal |
kubectl logs -n kube-system ds/eks-pod-identity-agent; the role’s trust policy |
| Pod Identity: agent cannot reach EKS Auth | private subnets without an interface endpoint for the EKS Auth API | VPC endpoints in the node subnets |
| Agent does not start | IPv6 disabled on the node while the agent binds [fd00:ec2::23] |
agent logs; the add-on setting that disables IPv6 |
| Association created, pod still has the old role | credentials are cached by the agent (6 hours, or 59 minutes with a target role), and associations are eventually consistent | restart the pod |
| Both mechanisms configured, the wrong one wins | the default chain stops at the first provider that works. Static keys in env come first, and web identity (IRSA) is tried before container credentials (Pod Identity) | env; remove the annotation or the stray keys |
Unable to locate credentials, then a long hang |
nothing injected and IMDS is not reachable from the pod (hop limit 1), which is the safe failure | fix the injection. Do not raise the hop limit |
| Non-root container cannot read the token file | file permissions on the projected volume | set securityContext.fsGroup on the pod |
| Java application ignores IRSA | the sts module is not on the classpath |
add the STS dependency |
PackedPolicyTooLarge (Pod Identity) |
session tags plus session policy exceed the packed size limit | disable session tags on the association |
IMDS and the hop limit. The fallback to the node role is the dangerous failure, because nothing errors. IMDSv2 needs a PUT to get a session token, and the response to that PUT has an IP hop limit. A pod on the pod network is one hop further away than the host. With a hop limit of 1 the response never reaches the pod, so the node role is out of reach. With 2, any pod can take the node’s credentials.
# what a node is set toaws ec2 describe-instances --instance-ids i-0abc123 \ --query 'Reservations[].Instances[].MetadataOptions'
# require IMDSv2 and keep the token on the hostaws ec2 modify-instance-metadata-options --instance-id i-0abc123 \ --http-tokens required --http-put-response-hop-limit 1
# from inside a pod: this should time outcurl -s -m 2 -X PUT http://169.254.169.254/latest/api/token \ -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' || echo blockedSet it in the launch template so new nodes get it. Pods with hostNetwork: true always reach IMDS, whatever the hop limit. Do not disable IMDS altogether: node components need it.
Test the Pod Identity path by hand:
curl -s -H "Authorization: $(cat "$AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")" \ "$AWS_CONTAINER_CREDENTIALS_FULL_URI" | head -c 200Test the IRSA path by hand:
aws sts assume-role-with-web-identity \ --role-arn "$AWS_ROLE_ARN" --role-session-name debug \ --web-identity-token "file://$AWS_WEB_IDENTITY_TOKEN_FILE" \ --query 'AssumedRoleUser.Arn'Keywords
Section titled “Keywords”IRSA, IAM Roles for Service Accounts, EKS Pod Identity, eks-pod-identity-agent, pod identity association, pods.eks.amazonaws.com, sts:AssumeRole, sts:TagSession, sts:AssumeRoleWithWebIdentity, AssumeRoleForPodIdentity, EKS Auth API, OIDC issuer, IAM OIDC provider, projected service account token, audience, sts.amazonaws.com, sub, aud, trust policy, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, 169.254.170.23, container credential provider, default credential chain, session tags, ABAC, aws:PrincipalTag, aws:RequestTag, target role, role chaining, node instance role, instance profile, IMDS, IMDSv2, hop limit, 169.254.169.254, kube2iam, kiam, Workload Identity Federation for GKE, GKE metadata server, Microsoft Entra Workload ID, federated identity credential, get-caller-identity.
Sources
Section titled “Sources”- Amazon EKS: Grant Kubernetes workloads access to AWS using service accounts (comparison of Pod Identity and IRSA, limits)
- Amazon EKS: Learn how EKS Pod Identity grants pods access to AWS services (agent address and ports, 5,000 association limit, restrictions)
- Amazon EKS: Understand how EKS Pod Identity works (injected variables and token)
- Amazon EKS: Set up the Amazon EKS Pod Identity Agent
- Amazon EKS: Create IAM role with trust policy required by EKS Pod Identity
- Amazon EKS: Grant pods access to AWS resources based on tags (session tags)
- Amazon EKS: Access AWS resources using EKS Pod Identity target IAM roles (cross-account, cache times)
- Amazon EKS: Use pod identity with the AWS SDK (minimum SDK versions)
- Amazon EKS: IAM roles for service accounts (OIDC background, key rotation)
- Amazon EKS: Authenticate to another account with IRSA
- Amazon EKS: Use IRSA with the AWS SDK
- amazon-eks-pod-identity-webhook (injected variables, annotations, defaults)
- EKS best practices: Identity and access management (IMDSv2 and hop limit)
- IAM and AWS STS quotas (OIDC providers, trust policy length, session tags)
- AWS SDKs and Tools: Standardized credential providers (credential provider chain)
- AWS Open Source Blog: Introducing fine-grained IAM roles for service accounts (the origin of IRSA, 4 September 2019)
- AWS What’s New: Amazon EKS Pod Identity (26 November 2023)
- Google Cloud: Workload Identity Federation for GKE
- Microsoft Learn: Microsoft Entra Workload ID on AKS