interview-prep

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."


My notes