The Security Gap
A pod running in EKS often needs to call AWS APIs — reading from S3, writing to DynamoDB, publishing to SNS. The question is: how does the pod prove its identity to AWS?
Bad approach 1 — Node IAM Role: Attach an IAM policy to the EC2 node's instance role. Every pod on that node inherits those permissions. If your node runs both a payment processor and a public-facing web server, both pods can access your payment data. This violates the Principle of Least Privilege.
Bad approach 2 — Hardcoded credentials: Store AWS Access Keys in environment variables or Kubernetes Secrets. Keys get committed to Git, leaked in logs, or forgotten and never rotated. This is a well-documented source of AWS account compromises.
The Solution: IRSA (IAM Roles for Service Accounts)
IRSA uses the OpenID Connect (OIDC) protocol to create a cryptographically verifiable link between a Kubernetes identity (a Service Account) and an AWS identity (an IAM Role). Each pod gets its own temporary, short-lived credentials — not shared with any other pod.
The IRSA Flow, Step by Step
Pod starts
→ kubelet injects a signed OIDC token into the pod's filesystem
→ Pod's SDK reads the token automatically
→ SDK calls AWS STS: "I have this token, give me credentials for Role X"
→ STS verifies the token signature against the cluster's OIDC provider
→ STS returns temporary credentials (valid 1 hour, auto-refreshed)
→ Pod calls S3/DynamoDB/etc. with those credentials
Setting Up IRSA: The Three Components
1. Associate an OIDC Provider with your cluster (one-time setup): a single eksctl call registers the cluster as a trusted identity provider with IAM — nothing to configure beyond the cluster name and an approval flag.
2. Create an IAM Role with a trust policy scoped to a specific Service Account:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:default:report-sa",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}]
}
The sub condition is critical — it pins the trust to a specific namespace and service account name. Without it, any service account in the cluster could assume the role.
3. Annotate the Kubernetes Service Account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: report-sa
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/FinanceReportRole
Result: Only pods using serviceAccountName: report-sa in the default namespace receive credentials for FinanceReportRole. Every other pod on the same node gets nothing.
Senior Insight: IRSA tokens are projected service account tokens with a 24-hour expiry by default, but the AWS SDK refreshes them automatically well before expiry. The token is mounted at /var/run/secrets/eks.amazonaws.com/serviceaccount/token — distinct from the standard Kubernetes service account token. If you see ExpiredTokenException errors in pods, the most common cause is a clock skew between the node and AWS STS, not an actual expiry issue.