Crisp answer: IRSA (IAM Roles for Service Accounts) lets Kubernetes pods assume AWS IAM roles via OIDC federation. Each pod's service account maps to a specific IAM role, giving it fine-grained AWS permissions without sharing an instance profile across all pods on the node.
Why IRSA matters:
Without IRSA, pods get permissions from the EC2 instance profile of the node they run on. All pods on that node share the same AWS credentials. A compromised pod can use the instance profile to access anything the node role allows.
With IRSA, each pod gets its own short-lived, scoped credentials via a projected service account token:
Old way (EC2 instance profile):
node role has: S3 full access + RDS full access + CloudWatch full access
→ every pod on the node can do all of that
IRSA way:
api-pod SA → api-role (S3 read on docs bucket only)
ingest-pod SA → ingest-role (S3 write + Bedrock invoke only)
monitoring-pod SA → monitoring-role (CloudWatch put metrics only)
Setup — step by step:
# 1. Enable OIDC provider for the cluster (one-time setup)
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--region eu-west-2 \
--approve
# Or with AWS CLI:
OIDC_URL=$(aws eks describe-cluster \
--name my-cluster \
--query "cluster.identity.oidc.issuer" \
--output text | sed 's|https://||')
aws iam create-open-id-connect-provider \
--url "https://${OIDC_URL}" \
--client-id-list sts.amazonaws.com \
--thumbprint-list $(openssl s_client -connect ${OIDC_URL}:443 \
-showcerts </dev/null 2>&1 | openssl x509 -fingerprint -sha1 -noout \
| sed 's/://g' | awk -F= '{print $2}')
# 2. Create an IAM role with a trust policy for the service account
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
OIDC_PROVIDER=$(aws eks describe-cluster \
--name my-cluster \
--query "cluster.identity.oidc.issuer" \
--output text | sed 's|https://||')
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": "system:serviceaccount:production:api-sa",
"${OIDC_PROVIDER}:aud": "sts.amazonaws.com"
}
}
}]
}
EOF
aws iam create-role \
--role-name api-irsa-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name api-irsa-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# 3. Annotate the Kubernetes service account with the role ARN
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/api-irsa-role
---
# 4. Reference the service account in the pod spec
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
serviceAccountName: api-sa # pod assumes the role via this SA
containers:
- name: api
image: myapp:v1
How the credentials are delivered:
The VPC CNI and EKS mutating webhook automatically mount a projected token into pods using annotated service accounts:
kubectl exec -it <pod> -- ls /var/run/secrets/eks.amazonaws.com/serviceaccount/
# token ← short-lived JWT, rotated automatically by kubelet
# The token contains claims like:
# sub: system:serviceaccount:production:api-sa
# AWS SDK reads this token and calls sts:AssumeRoleWithWebIdentity
Terraform for IRSA:
data "aws_iam_openid_connect_provider" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_role" "api" {
name = "api-irsa-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${data.aws_iam_openid_connect_provider.eks.url}:sub" =
"system:serviceaccount:production:api-sa"
"${data.aws_iam_openid_connect_provider.eks.url}:aud" =
"sts.amazonaws.com"
}
}
}]
})
}
Debugging IRSA:
# Check if pod has the token mounted
kubectl exec -it <pod> -- env | grep AWS
# AWS_ROLE_ARN=arn:aws:iam::123456789:role/api-irsa-role
# AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Test the role assumption from within the pod
kubectl exec -it <pod> -- aws sts get-caller-identity
# Should show the IRSA role, not the node role
# Common issue: trust policy condition doesn't match
# Check the exact namespace and SA name in the Condition block
What to say in the interview:
"IRSA is the correct way to give pods AWS permissions on EKS. Without it every pod on a node shares the node's instance profile which violates least privilege. IRSA uses OIDC federation: the cluster has an OIDC provider, each service account is annotated with an IAM role ARN, and when a pod starts the mutating webhook mounts a short-lived token. The AWS SDK reads that token and calls sts:AssumeRoleWithWebIdentity to get temporary credentials scoped to that specific role. The trust policy must match the exact namespace and service account name. I set this up with Terraform in the rag-bedrock project for Lambda but the same pattern applies to EKS pods."