1. Introduction
If your pods need to interact with AWS services — reading from S3, writing to DynamoDB, pulling images from ECR, sending messages via SQS — they need AWS credentials. The naive approach is to create an IAM user, generate an access key, and inject it as a Kubernetes secret. It works, but it creates long-lived credentials that are hard to rotate, easy to leak, and impossible to scope to a specific pod.
IAM Roles for Service Accounts (IRSA) solves all of this. It lets you bind a Kubernetes service account to an IAM role using OIDC federation. When a pod uses that service account, AWS automatically provides short-lived, role-scoped credentials through the pod's environment — no static secrets, no node-level permissions shared across all pods, no credential rotation to manage. For ECR authentication specifically, IRSA eliminates the 12-hour token expiry problem described in ECR Login Failed: Fix AWS ECR Authentication.
This guide walks through the complete IRSA setup from scratch: enabling the OIDC provider, creating the IAM role with the right trust policy, annotating the Kubernetes service account, and verifying it works end to end.
2. When and Why to Use IRSA
Use IRSA whenever a pod needs to call AWS APIs. Common scenarios include:
- Pods reading from or writing to S3 buckets
- Applications querying DynamoDB or RDS via the AWS SDK
- Workloads publishing to or consuming from SNS/SQS
- Pods pulling images from private ECR repositories without static imagePullSecrets (resolves the root cause behind ImagePullBackOff errors caused by expired ECR tokens)
- Controllers and operators that manage AWS resources (e.g. AWS Load Balancer Controller, ExternalDNS, Cluster Autoscaler)
- Velero or other backup tools that write to S3
IRSA is specifically the right choice over alternatives in these situations:
| Approach | Problem | Why IRSA is better |
|---|---|---|
| Static IAM user + Kubernetes secret | Credentials never expire, hard to rotate, leak risk | IRSA tokens are short-lived (1h) and auto-rotated |
| EC2 instance profile on worker nodes | All pods on the node share the same IAM permissions | IRSA scopes permissions per service account / pod |
| kube2iam / kiam | Deprecated patterns with added complexity and race conditions | IRSA is the AWS-native solution, no sidecar needed |
3. Prerequisites
- An existing EKS cluster (EKS 1.13 or later — IRSA has been supported since 1.13)
- AWS CLI v2 installed and configured with permissions to create IAM roles and policies
- eksctl installed (optional but used in examples — all steps have AWS CLI equivalents)
- kubectl configured to connect to your EKS cluster
- The AWS account ID and EKS cluster name handy — used throughout the setup
# Confirm your cluster context kubectl config current-context
# Confirm AWS CLI identity aws sts get-caller-identity
# Set variables used throughout this guide export CLUSTER_NAME="my-eks-cluster" export AWS_REGION="us-east-1" export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) export NAMESPACE="my-app" export SERVICE_ACCOUNT_NAME="my-app-sa" export IAM_ROLE_NAME="my-app-irsa-role"
4. Step-by-Step Implementation
Step 1: Enable the OIDC Identity Provider for your cluster
IRSA works through OpenID Connect (OIDC) federation. EKS creates an OIDC issuer URL for your cluster, but you need to register it as an IAM Identity Provider in your AWS account before IAM can trust tokens issued by it.
Check if the OIDC provider already exists:
# Get the OIDC issuer URL for your cluster aws eks describe-cluster \ --name $CLUSTER_NAME \ --region $AWS_REGION \ --query 'cluster.identity.oidc.issuer' \ --output text
# Example output:
# https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
# Extract the OIDC ID (the part after /id/) export OIDC_ID=$(aws eks describe-cluster \ --name $CLUSTER_NAME --region $AWS_REGION \ --query 'cluster.identity.oidc.issuer' --output text \ | cut -d '/' -f 5)
# Check if the provider is already registered in IAM aws iam list-open-id-connect-providers \ | grep $OIDC_ID
If the provider is not registered, create it:
# Using eksctl (recommended — handles the thumbprint automatically) eksctl utils associate-iam-oidc-provider \ --cluster $CLUSTER_NAME \ --region $AWS_REGION \ --approve
# Using AWS CLI directly export OIDC_URL=$(aws eks describe-cluster \ --name $CLUSTER_NAME --region $AWS_REGION \ --query 'cluster.identity.oidc.issuer' --output text)
# Get the certificate thumbprint export THUMBPRINT=$(echo | openssl s_client \ -servername oidc.eks.${AWS_REGION}.amazonaws.com \ -showcerts -connect oidc.eks.${AWS_REGION}.amazonaws.com:443 2>/dev/null \ | openssl x509 -fingerprint -noout \ | sed 's/SHA1 Fingerprint=//' | tr -d ':' | tr '[:upper:]' '[:lower:]') aws iam create-open-id-connect-provider \ --url $OIDC_URL \ --client-id-list sts.amazonaws.com \ --thumbprint-list $THUMBPRINT
Step 2: Create the IAM Policy
Define exactly what AWS permissions your pod needs. The principle of least privilege applies here — only grant the specific actions on the specific resources the workload requires.
This example grants a pod read-only access to a specific S3 bucket:
# Create the policy document cat > /tmp/irsa-policy.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::my-app-bucket", "arn:aws:s3:::my-app-bucket/*" ] } ] } EOF
# Create the IAM policy aws iam create-policy \ --policy-name my-app-s3-policy \ --policy-document file:///tmp/irsa-policy.json
# Store the policy ARN export POLICY_ARN=$(aws iam list-policies \ --query 'Policies[?PolicyName==`my-app-s3-policy`].Arn' \ --output text)
Step 3: Create the IAM Role with a Trust Policy
This is the core of IRSA. The IAM role's trust policy must explicitly allow the EKS OIDC provider to assume it, scoped to the specific Kubernetes namespace and service account name. This is what prevents other pods in the same cluster from using the same role.
# Build the trust policy document cat > /tmp/trust-policy.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:sub": "system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT_NAME}", "oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:aud": "sts.amazonaws.com" } } } ] } EOF
# Create the IAM role aws iam create-role \ --role-name $IAM_ROLE_NAME \ --assume-role-policy-document file:///tmp/trust-policy.json \ --description "IRSA role for ${SERVICE_ACCOUNT_NAME} in ${NAMESPACE}"
# Attach the policy to the role aws iam attach-role-policy \ --role-name $IAM_ROLE_NAME \ --policy-arn $POLICY_ARN
# Store the role ARN export ROLE_ARN=$(aws iam get-role \ --role-name $IAM_ROLE_NAME \ --query 'Role.Arn' --output text)
Step 4: Create and Annotate the Kubernetes Service Account
Create the Kubernetes service account and annotate it with the IAM role ARN. The annotation is how EKS knows which IAM role to associate with which service account.
Option A — Using kubectl:
# Create the namespace if it doesn't exist kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# Create the service account with the IRSA annotation kubectl create serviceaccount $SERVICE_ACCOUNT_NAME -n $NAMESPACE kubectl annotate serviceaccount $SERVICE_ACCOUNT_NAME \ -n $NAMESPACE \ eks.amazonaws.com/role-arn=$ROLE_ARN
# Verify the annotation was applied kubectl describe serviceaccount $SERVICE_ACCOUNT_NAME -n $NAMESPACE
Option B — Declarative YAML (recommended for GitOps):
apiVersion: v1
kind: ServiceAccount metadata: name: my-app-sa namespace: my-app annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-irsa-role
Option C — Using eksctl (creates both IAM role and service account together):
eksctl create iamserviceaccount \ --cluster $CLUSTER_NAME \ --namespace $NAMESPACE \ --name $SERVICE_ACCOUNT_NAME \ --role-name $IAM_ROLE_NAME \ --attach-policy-arn $POLICY_ARN \ --region $AWS_REGION \ --approve
# eksctl handles steps 1-4 in one command:
# - OIDC provider association (if not already done)
# - Trust policy creation with correct conditions
# - IAM role creation and policy attachment
# - Kubernetes service account creation with annotation
Step 5: Configure your Pod to use the Service Account
Reference the annotated service account in your pod or Deployment spec. The EKS admission controller will automatically inject the IRSA environment variables and a projected volume with the OIDC token.
apiVersion: apps/v1
kind: Deployment metadata: name: my-app namespace: my-app spec: replicas: 1 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: serviceAccountName: my-app-sa # <-- reference your annotated SA here containers: - name: my-app image: my-app:latest # No AWS credentials needed in env — IRSA handles this automatically
When the pod starts, the EKS Pod Identity Webhook (running in kube-system) injects these environment variables automatically:
# Automatically injected by the EKS webhook: AWS_ROLE_ARN=arn:aws:iam::123456789012:role/my-app-irsa-role AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# The AWS SDK picks these up automatically — no code changes needed
# The token file is a projected volume mounted at that path
# Tokens are automatically rotated by the kubelet before they expire
5. Testing and Validation
Before trusting IRSA in production, verify the credential flow end to end. If pods fail to schedule during setup, see Pod Pending: No Nodes Available for the most common node group and taint issues on EKS.
Verify the injected environment inside the pod
# Exec into the running pod kubectl exec -it deployment/my-app -n $NAMESPACE -- /bin/sh
# Inside the pod — confirm the IRSA env vars are present echo $AWS_ROLE_ARN echo $AWS_WEB_IDENTITY_TOKEN_FILE
# Confirm the token file exists and is non-empty ls -la $AWS_WEB_IDENTITY_TOKEN_FILE cat $AWS_WEB_IDENTITY_TOKEN_FILE | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# The decoded token should show sub: system:serviceaccount:<namespace>:<sa-name>
Test the assumed role identity
# Install AWS CLI in your test pod, or use an aws-cli image kubectl run irsa-test \ --image=amazon/aws-cli \ --restart=Never \ --serviceaccount=$SERVICE_ACCOUNT_NAME \ -n $NAMESPACE \ -- sts get-caller-identity
# Expected output:
# {
# "UserId": "AROAEXAMPLE:botocore-session-...",
# "Account": "123456789012",
# "Arn": "arn:aws:sts::123456789012:assumed-role/my-app-irsa-role/..."
# }
# Clean up the test pod after verifying kubectl delete pod irsa-test -n $NAMESPACE
Test the actual AWS permission
# Test S3 access from inside the pod (example) kubectl exec -it deployment/my-app -n $NAMESPACE -- \ aws s3 ls s3://my-app-bucket/ --region $AWS_REGION
# Test that permissions outside the policy are denied kubectl exec -it deployment/my-app -n $NAMESPACE -- \ aws s3 ls # should fail — no ListAllMyBuckets permission
6. Common Issues
'An error occurred (AccessDenied): Not authorized to perform sts:AssumeRoleWithWebIdentity'
The trust policy condition doesn't match the service account being used. Check:
- The namespace in the trust policy sub condition matches the actual pod namespace exactly
- The service account name in the trust policy matches the annotated service account exactly
- The OIDC ID in the trust policy matches the cluster's OIDC provider ID
- The service account annotation contains the correct role ARN with no trailing spaces
# Re-check the trust policy conditions aws iam get-role --role-name $IAM_ROLE_NAME \ --query 'Role.AssumeRolePolicyDocument' --output json
# Re-check the service account annotation kubectl get sa $SERVICE_ACCOUNT_NAME -n $NAMESPACE -o yaml
Pod environment does not contain AWS_ROLE_ARN
The EKS Pod Identity Webhook is not running or the service account annotation is missing:
# Check the webhook is running in kube-system kubectl get pods -n kube-system | grep pod-identity-webhook
# Confirm the service account has the annotation kubectl get sa $SERVICE_ACCOUNT_NAME -n $NAMESPACE \ -o jsonpath='{.metadata.annotations}'
# Restart the pod to trigger fresh webhook injection kubectl rollout restart deployment/my-app -n $NAMESPACE
AWS SDK not picking up IRSA credentials
If the SDK still uses node-level instance profile credentials instead of the IRSA role, it may be overriding the default credential chain with an explicit provider. Check:
- No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variables are set on the container — these take precedence over WebIdentity credentials in the default chain
- The SDK version supports WebIdentityToken credential provider — older SDK versions may not
- The application is not explicitly constructing a credential provider that bypasses the default chain
7. Summary
IRSA is the correct way to give EKS pods access to AWS services. Static IAM user credentials and node-level instance profiles both have significant security and operational drawbacks that IRSA avoids entirely.
The complete setup in five steps:
- Enable the OIDC provider — associate your cluster's OIDC issuer URL with IAM
- Create an IAM policy — define the minimum permissions the pod needs
- Create an IAM role with a scoped trust policy — use the sub condition to lock it to a specific service account and namespace
- Annotate the Kubernetes service account — add the eks.amazonaws.com/role-arn annotation
- Reference the service account in your pod spec — the webhook injects credentials automatically
| Component | What it does | Common mistake |
|---|---|---|
| OIDC provider | Lets IAM trust tokens issued by EKS | Not registering it — IRSA silently fails |
| Trust policy :sub | Scopes the role to one service account | Missing condition — any SA can assume the role |
| Trust policy :aud | Validates the token audience is sts.amazonaws.com | Omitting it — trust policy may be too broad |
| SA annotation | Tells the webhook which role to inject | Typo in ARN — no injection, no error message |
| serviceAccountName | Connects the pod to the annotated SA | Using default SA — gets no IRSA injection |