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:

IRSA is specifically the right choice over alternatives in these situations:

ApproachProblemWhy IRSA is better
Static IAM user + Kubernetes secretCredentials never expire, hard to rotate, leak riskIRSA tokens are short-lived (1h) and auto-rotated
EC2 instance profile on worker nodesAll pods on the node share the same IAM permissionsIRSA scopes permissions per service account / pod
kube2iam / kiamDeprecated patterns with added complexity and race conditionsIRSA is the AWS-native solution, no sidecar needed

3. Prerequisites

# 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:

# 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:

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:

ComponentWhat it doesCommon mistake
OIDC providerLets IAM trust tokens issued by EKSNot registering it — IRSA silently fails
Trust policy :subScopes the role to one service accountMissing condition — any SA can assume the role
Trust policy :audValidates the token audience is sts.amazonaws.comOmitting it — trust policy may be too broad
SA annotationTells the webhook which role to injectTypo in ARN — no injection, no error message
serviceAccountNameConnects the pod to the annotated SAUsing default SA — gets no IRSA injection