Originally published on kuryzhev.cloud
A pod in an EKS cluster needs to read from S3, and someone on the team pastes an AWS access key into a Kubernetes secret because it's the fastest way to unblock a deploy. This is the moment IAM roles for service accounts, better known as IRSA, exists to prevent. Instead of shipping long-lived credentials into a cluster, IRSA lets a pod exchange a short-lived token for temporary AWS credentials tied to a specific IAM role. No keys stored anywhere, no rotation cron job, no secret sitting in etcd waiting to leak.
The mechanism is not magic, and understanding the moving parts is what separates teams that configure IRSA correctly from teams that burn time debugging AccessDenied errors that appear to make no sense. This explainer walks through what IRSA actually does under the hood, the failure modes that get reported most often, the setup that avoids them, and where the pattern gets interesting once you're running many workloads across multiple accounts.
What this actually does
EKS clusters expose an OIDC (OpenID Connect) issuer URL, which is effectively an identity provider that AWS IAM can trust. When you enable IRSA, you register that OIDC provider with IAM, then create an IAM role with a trust policy scoped to a specific Kubernetes namespace and service account name. The pod's service account gets annotated with the role's ARN, and the EKS pod identity webhook injects a projected service account token volume and environment variables into any pod using that service account.
At runtime, the AWS SDK inside the container reads AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN, calls STS's AssumeRoleWithWebIdentity, and gets back temporary credentials. Their lifetime depends on the role's configured maximum session duration and on what the SDK requests. The SDK handles refresh automatically as long as it supports the web identity credential provider — current major versions do, including AWS SDK for JavaScript v3, boto3, and AWS SDK for Go v2. Very old SDK majors may not, so check the SDK version if credentials never appear.
The important detail: the token is a JWT signed by the cluster's OIDC issuer, and STS validates that signature against the keys the issuer publishes at its JWKS endpoint. The thumbprint you supply when registering the provider pins the TLS certificate chain used to reach that endpoint. There is no AWS-side database mapping pods to roles. Trust lives entirely in the IAM role's trust policy conditions, which check the token's subject claim (namespace and service account) and audience claim. Get those conditions wrong and you either lock everyone out or, worse, open the role to more service accounts than intended. See the official IRSA documentation for the exact trust policy shape AWS expects.
How people use it wrong
A commonly reported failure is copying a trust policy from one project to another without updating the namespace or service account name in the condition. The role assumes fine in the environment it was written for, then fails in the next one because the condition still references staging-app. The error returned to the pod is a generic AccessDenied from STS, which gives almost no hint about which part of the trust condition didn't match.
Watch out for using StringEquals when the intent was to allow multiple service accounts with a wildcard. IAM condition operators compare exact strings unless you explicitly use StringLike. A typical failure pattern: someone adds a second service account to a namespace, assumes it inherits the same role because it's in the same namespace, and is surprised when it can't assume anything.
Another frequent mistake is granting IAM permissions far broader than the workload needs, on the theory that IRSA itself is "secure enough" so the attached policy doesn't need much scrutiny. IRSA controls who can assume the role, not what the role can do once assumed. A role trusted only by one exact service account can still have an AdministratorAccess policy attached, which defeats most of the point.
Watch out for pods that don't restart after a service account annotation changes. The webhook injects the token volume and environment variables at pod creation time, so updating the IAM role ARN on an existing service account does nothing until pods are recreated — a rolling restart or deployment rollout is required, and this is easy to forget mid-incident.
The correct approach
Start with least privilege on the IAM policy side, scoped to specific resource ARNs rather than *. Then build the trust policy to match exactly one namespace and service account per role wherever practical — this keeps blast radius contained and makes audits straightforward.
A minimal trust policy for a service account named s3-reader in namespace data-pipeline looks like this. Verify the OIDC provider ARN and issuer URL against your cluster with aws eks describe-cluster --name <cluster> --query "cluster.identity.oidc.issuer" before pasting.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.eu-central-1.amazonaws.com/id/EXAMPLE:sub": "system:serviceaccount:data-pipeline:s3-reader",
"oidc.eks.eu-central-1.amazonaws.com/id/EXAMPLE:aud": "sts.amazonaws.com"
}
}
}
]
}
The service account itself only needs the role ARN annotation. This is the piece that actually wires a pod to the role at admission time.
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-reader
namespace: data-pipeline
annotations:
# this ARN must point at a role whose trust policy matches this namespace and name
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/s3-reader-role
Always include the aud condition, not just sub. The audience claim is what ties the token to STS as its intended recipient; checking it prevents a token minted for a different audience from satisfying the condition. Cross-check your Terraform or CloudFormation modules against the current Kubernetes service account documentation, since projected token behavior has changed across cluster versions.
Advanced patterns
Multi-account setups are where IRSA gets genuinely interesting. A cluster in account A can assume a role in account B if account B has the cluster's OIDC issuer registered as an identity provider and the role's trust policy references that provider with the same sub and aud conditions. No role chaining through account A's IAM is required. This is useful for centralized logging or shared data-lake accounts where the EKS cluster lives in a workload account but needs to write to a platform account's S3 bucket.
EKS Pod Identity is now generally available and is AWS's recommended default for new EKS clusters. It removes the OIDC trust policy work in favor of an EKS-managed association between a service account and a role, configured through the EKS API rather than IAM trust conditions. IRSA remains fully supported and is still the more portable pattern if you run workloads outside EKS or on Kubernetes distributions that have no Pod Identity agent.
For platform teams managing many roles, a naming convention like irsa-<namespace>-<serviceaccount> combined with a Terraform module that generates both the role and the trust policy from the same two input variables prevents the copy-paste namespace mismatch described earlier. Pairing this with OPA/Gatekeeper or Kyverno policies that reject service account annotations pointing at roles outside an approved naming pattern catches drift before it reaches production. If you're standardizing this across a platform, the DevOps_DayS archive covers related Kubernetes hardening patterns worth pairing with an IRSA rollout.
Performance notes
IRSA's overhead is mostly invisible day-to-day but not zero. The projected token has a bounded lifetime set by the pod identity webhook, and the kubelet rotates it before expiry. The SDK caches STS credentials until close to expiry, so steady-state API calls don't pay an extra round trip per request. The cost shows up at pod startup: the first AWS SDK call in a freshly started container blocks on an AssumeRoleWithWebIdentity call, adding one network round trip before any actual AWS API work begins.
For latency-sensitive cold-start workloads — short-lived Jobs, for example — that STS round trip can matter. One practical option is to initialize the credential provider early in application startup so the exchange overlaps with other initialization work; the broader mitigation is keeping pods long-running where possible rather than spinning up a fresh pod per unit of work.
At scale, watch STS throttling. STS applies regional request limits, and a cluster where many pods restart simultaneously during a rolling node replacement can generate a burst of AssumeRoleWithWebIdentity calls. Whether this becomes a problem depends heavily on cluster size, workload churn, and what else in the account calls STS. During large node group upgrades it's worth checking CloudTrail for throttling errors rather than assuming AccessDenied always means a policy misconfiguration. Verify with CloudTrail event history filtered on the STS API before concluding it's an IAM roles for service accounts trust issue versus a rate limit.
Top comments (0)