Setting Up IAM Roles for Service Accounts (IRSA) on EKS
Give individual Kubernetes pods scoped AWS permissions without node-level IAM roles. IRSA uses the EKS OIDC provider to issue short-lived credentials per ServiceAccount — no static keys, no overly permissive nodes.
Before you begin
- An EKS cluster with an OIDC provider enabled
- AWS CLI configured with IAM admin permissions
- kubectl configured for the cluster
- eksctl (optional but makes OIDC setup simpler)
The naive approach to giving pods AWS access: attach IAM policies to the node group IAM role. This gives every pod on every node the same permissions. One compromised pod means access to everything.
IRSA (IAM Roles for Service Accounts) scopes permissions to a specific ServiceAccount in a specific namespace. A pod in production with ServiceAccount s3-writer can write to S3. The pod next to it with ServiceAccount my-api cannot.
How IRSA Works
- EKS has an OIDC provider — a URL that proves "this ServiceAccount token came from this cluster"
- You create an IAM role with a trust policy that says "allow this role to be assumed if the token is from this specific ServiceAccount in this namespace"
- Kubernetes mounts a projected token (not the default SA token) into the pod
- The AWS SDK exchanges that token for temporary credentials automatically
Your application code doesn't change — boto3, the AWS SDK for Go, or any other AWS SDK picks up the credentials from environment variables that the EKS admission controller injects.
Step 1: Verify the OIDC Provider Exists
1# Get cluster OIDC issuer URL
2aws eks describe-cluster \
3 --name my-cluster \
4 --query "cluster.identity.oidc.issuer" \
5 --output text
6# https://oidc.eks.ap-south-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E
7
8# Check if an IAM OIDC provider exists for this URL
9aws iam list-open-id-connect-providersIf no provider exists for your cluster's OIDC URL, create it:
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--region ap-south-1 \
--approveOr manually:
1OIDC_URL=$(aws eks describe-cluster \
2 --name my-cluster \
3 --query "cluster.identity.oidc.issuer" \
4 --output text | sed 's|https://||')
5
6THUMBPRINT=$(openssl s_client -connect oidc.eks.ap-south-1.amazonaws.com:443 2>/dev/null \
7 | openssl x509 -fingerprint -noout -sha1 \
8 | sed 's/://g' \
9 | awk -F= '{print tolower($2)}')
10
11aws iam create-open-id-connect-provider \
12 --url "https://${OIDC_URL}" \
13 --client-id-list sts.amazonaws.com \
14 --thumbprint-list $THUMBPRINTStep 2: Create the IAM Policy
Define what the pod is allowed to do:
1# Example: write-only access to a specific S3 bucket
2cat > s3-writer-policy.json <<EOF
3{
4 "Version": "2012-10-17",
5 "Statement": [
6 {
7 "Effect": "Allow",
8 "Action": [
9 "s3:PutObject",
10 "s3:PutObjectAcl"
11 ],
12 "Resource": "arn:aws:s3:::my-app-uploads/*"
13 },
14 {
15 "Effect": "Allow",
16 "Action": [
17 "s3:ListBucket"
18 ],
19 "Resource": "arn:aws:s3:::my-app-uploads"
20 }
21 ]
22}
23EOF
24
25aws iam create-policy \
26 --policy-name S3WriterPolicy \
27 --policy-document file://s3-writer-policy.jsonStep 3: Create the IAM Role with a Trust Policy
The trust policy says: "allow this role to be assumed by the OIDC token if it's from ServiceAccount s3-writer in namespace production."
1ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
2OIDC_URL=$(aws eks describe-cluster \
3 --name my-cluster \
4 --query "cluster.identity.oidc.issuer" \
5 --output text | sed 's|https://||')
6
7cat > trust-policy.json <<EOF
8{
9 "Version": "2012-10-17",
10 "Statement": [
11 {
12 "Effect": "Allow",
13 "Principal": {
14 "Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_URL}"
15 },
16 "Action": "sts:AssumeRoleWithWebIdentity",
17 "Condition": {
18 "StringEquals": {
19 "${OIDC_URL}:sub": "system:serviceaccount:production:s3-writer",
20 "${OIDC_URL}:aud": "sts.amazonaws.com"
21 }
22 }
23 }
24 ]
25}
26EOF
27
28aws iam create-role \
29 --role-name S3WriterRole \
30 --assume-role-policy-document file://trust-policy.json
31
32aws iam attach-role-policy \
33 --role-name S3WriterRole \
34 --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/S3WriterPolicy
35
36ROLE_ARN=$(aws iam get-role \
37 --role-name S3WriterRole \
38 --query "Role.Arn" --output text)
39echo "Role ARN: $ROLE_ARN"The sub claim format is always system:serviceaccount:<namespace>:<service-account-name>. This is the key trust condition — it scopes the role to exactly one ServiceAccount in one namespace.
Step 4: Create the Kubernetes ServiceAccount
Annotate the ServiceAccount with the IAM role ARN:
1kubectl create namespace production 2>/dev/null || true
2
3kubectl create serviceaccount s3-writer -n production
4
5kubectl annotate serviceaccount s3-writer \
6 -n production \
7 eks.amazonaws.com/role-arn=$ROLE_ARNOr declaratively:
1apiVersion: v1
2kind: ServiceAccount
3metadata:
4 name: s3-writer
5 namespace: production
6 annotations:
7 eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/S3WriterRole"
8 eks.amazonaws.com/token-expiration: "86400" # Token TTL in seconds (default: 86400)Step 5: Use the ServiceAccount in a Pod
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: my-app
5 namespace: production
6spec:
7 template:
8 spec:
9 serviceAccountName: s3-writer # This is the key field
10 containers:
11 - name: app
12 image: my-app:latest
13 env:
14 - name: AWS_REGION
15 value: ap-south-1
16 - name: S3_BUCKET
17 value: my-app-uploadsWhen the pod starts, EKS automatically:
- Mounts a projected token at
/var/run/secrets/eks.amazonaws.com/serviceaccount/token - Sets
AWS_WEB_IDENTITY_TOKEN_FILEenv var pointing to the token - Sets
AWS_ROLE_ARNenv var to the IAM role from the annotation
The AWS SDK reads these environment variables and handles credential exchange automatically.
Step 6: Verify It Works
Deploy a test pod and check its credentials:
1kubectl run irsa-test \
2 --image=amazon/aws-cli:latest \
3 --serviceaccount=s3-writer \
4 --namespace=production \
5 --rm -it \
6 --restart=Never \
7 -- sts get-caller-identityExpected output:
{
"UserId": "AROAEXAMPLE:eks-production-s3-writ-xxxx",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/S3WriterRole/eks-production-s3-writ-xxxx"
}The Arn confirms the pod assumed S3WriterRole. Test the actual permission:
1kubectl run irsa-test \
2 --image=amazon/aws-cli:latest \
3 --serviceaccount=s3-writer \
4 --namespace=production \
5 --rm -it \
6 --restart=Never \
7 -- s3 cp /etc/hostname s3://my-app-uploads/test.txtStep 7: Remove Node-Level IAM Permissions
After verifying IRSA works for all your services, remove overly permissive node-level IAM policies. Only these three are required on the node group role:
AmazonEKSWorkerNodePolicy — worker node lifecycle
AmazonEKS_CNI_Policy — VPC networking
AmazonEC2ContainerRegistryReadOnly — pulling images from ECR
Remove anything else (S3, DynamoDB, SQS, etc.) — those belong on per-service IRSA roles.
Common Patterns
Multiple services, different roles:
# Each service gets its own ServiceAccount + IAM role
kubectl annotate sa my-api -n production eks.amazonaws.com/role-arn=$MY_API_ROLE_ARN
kubectl annotate sa worker -n production eks.amazonaws.com/role-arn=$WORKER_ROLE_ARN
kubectl annotate sa exporter -n production eks.amazonaws.com/role-arn=$EXPORTER_ROLE_ARNCross-account access: The trust policy can reference an OIDC provider in account A while the role lives in account B. Add the cross-account OIDC ARN to the federated principal.
Token expiration tuning: For long-running batch jobs, extend the token TTL:
eks.amazonaws.com/token-expiration: "43200" # 12 hoursDebugging
NoCredentialProviders: The pod isn't using the annotated ServiceAccount. Check kubectl get pod my-pod -o yaml | grep serviceAccountName.
AssumeRoleWithWebIdentity: InvalidIdentityToken: The OIDC URL in the trust policy doesn't match the cluster's OIDC issuer. Double-check with aws eks describe-cluster --name my-cluster --query cluster.identity.oidc.issuer.
AccessDenied: The role is assumed correctly but doesn't have permission for the specific action. Verify with aws iam simulate-principal-policy.
Official References
- IAM Roles for Service Accounts — Official AWS EKS docs for IRSA: prerequisites, setup, and troubleshooting
- EKS Pod Identity — The newer AWS-native alternative to IRSA, simpler to configure for EKS-managed clusters
- OIDC Federation with IAM — How IAM OIDC identity providers work, which underpins IRSA
- Configuring a Kubernetes Service Account to Assume an IAM Role — Step-by-step from AWS on annotating ServiceAccounts and scoping IAM policies
- AWS eksctl IRSA — Using eksctl to create IRSA-enabled ServiceAccounts declaratively
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.