EKS Pod Identity vs IRSA
EKS Pod Identity binds an IAM role to a namespace and service account. Trust policy, leftover IRSA annotations, and an agent that fails closed.
aws eks create-addon --cluster-name payments --addon-name eks-pod-identity-agent
aws eks create-pod-identity-association \
--cluster-name payments \
--namespace payments \
--service-account payments-api \
--role-arn arn:aws:iam::123456789012:role/payments-api
That association is EKS Pod Identity. There is no eks.amazonaws.com/role-arn annotation and no per-cluster OIDC provider. The agent on the node exchanges the pod’s identity for STS credentials. What that role can do once assumed is the pod-to-cloud-admin path. This page is the binding.
Behavior and the trust-policy shape are documented in EKS Pod Identity.
Trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"],
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012",
"aws:RequestTag/kubernetes-namespace": "payments",
"aws:RequestTag/kubernetes-service-account": "payments-api"
},
"ArnEquals": {
"aws:SourceArn": "arn:aws:eks:us-east-1:123456789012:cluster/payments"
}
}
}
]
}
sts:TagSession is required. EKS sets session tags (kubernetes-namespace, kubernetes-service-account, eks-cluster-name). A trust policy copied from an old EC2 service role that only allows sts:AssumeRole will not work.
The association is what EKS will attempt. The request-tag conditions are what stop a second association from reusing this role: EKS sets kubernetes-namespace and kubernetes-service-account on the assume. aws:SourceArn stops a different cluster. Without the tags, any service account in payments that someone can associate will get the role. Treat eks:CreatePodIdentityAssociation and eks:DeletePodIdentityAssociation like iam:PassRole: cluster-admin and CI only, not every namespace developer. Session tags can be turned off on an association; if you disable them, these request-tag conditions stop matching and the assume fails. Leave tags on.
IRSA’s trust looks nothing like this. It is AssumeRoleWithWebIdentity against oidc.eks.<region>.amazonaws.com/id/<id> with sub = system:serviceaccount:payments:payments-api and :aud = sts.amazonaws.com. What that aud condition does and does not pin is IRSA trust policy aud. Reusing the IRSA document for Pod Identity fails closed. Leaving it in place beside the new trust is how one role stays assumable two ways.
What IRSA still is
| Pod Identity | IRSA | |
|---|---|---|
| Binding | create-pod-identity-association |
Annotation on the ServiceAccount |
| IAM principal | pods.eks.amazonaws.com |
Cluster OIDC provider |
| OIDC provider per cluster | No | Yes |
| Credential helper | eks-pod-identity-agent DaemonSet |
Projected SA token, exchanged at STS |
| Fargate | Only on platform versions that include the agent | Works without a node agent |
Do not install the agent and assume every Fargate profile picked it up. A profile on an older platform version still needs IRSA. Check the platform version before you delete the annotation from a Fargate service account.
Same role, many clusters: create one association per cluster. Without aws:SourceArn, any cluster in the account whose admin can create an association can use the role. That is convenient for a read-only telemetry role and a bad default for payments-api.
Leftover annotations
While the association exists, the webhook prefers Pod Identity over IRSA. The annotation does not get cleared. The agent being down does not switch the pod back to IRSA. New pods still get Pod Identity environment variables and then fail to fetch credentials. IRSA returns only if the association is removed and the annotation is still there.
kubectl get sa -A -o json \
| jq -r '.items[]
| select(.metadata.annotations["eks.amazonaws.com/role-arn"] != null)
| [.metadata.namespace, .metadata.name, .metadata.annotations["eks.amazonaws.com/role-arn"]]
| @tsv'
Diff that list against associations:
aws eks list-pod-identity-associations --cluster-name payments \
--query 'associations[].associationId' --output text
Then aws eks describe-pod-identity-association for each id. A service account with an annotation and no association is still IRSA. A service account with both is Pod Identity until the association is deleted, at which point the annotation is live again. Delete the annotation in the same change that creates the association.
When the agent is down
The addon is a DaemonSet. Taints, a bad node selector, or a broken CNI mean pods on that node cannot mint credentials. The SDK error is a credential failure, not a Kubernetes error.
The dangerous fallback is application code (or an old AWS SDK default) that then reads instance metadata. The node role is now the app. IMDSv2 hop limit 1 makes that fallback fail. Do not “fix” the outage by raising hop limit. Fix the DaemonSet, and keep the node role unable to read the app’s buckets.
Identity scope of payments-api is still CIEM: Pod Identity does not shrink s3:*.
Checklist
- Addon
eks-pod-identity-agenthealthy on every node that runs associated pods - Fargate platform version confirmed before removing IRSA from Fargate service accounts
- Trust policy allows
sts:AssumeRoleandsts:TagSession, withaws:SourceArnplus namespace and service-account request tags - Association names the namespace and service account; no second association to a broader role
-
eks.amazonaws.com/role-arnremoved from service accounts that have an association -
eks:CreatePodIdentityAssociationnot granted to namespace developers - Node role cannot read app data; hop limit stays 1 so IMDS is not the fallback
The association is the edge from the service account to the role. The role policy is still the blast radius.