EKS IRSA and Pod Identity: Fine-Grained AWS Permissions Without Static Keys

Posted: | Last updated: | 9 minute read

EKS IRSA and Pod Identity Architecture: How Kubernetes pods get AWS permissions through OIDC, IAM roles, and STS temporary credentials without static keys

Picture this: it’s 3 AM, your security team just flagged an exposed AWS access key buried in a ConfigMap that somehow made it to GitHub, and your S3 buckets are being scraped by someone in a country you’ve never deployed to. Static credentials in containers are a disaster waiting to happen. There’s a better way.

What Are IRSA and Pod Identity?

IAM Roles for Service Accounts (IRSA) and EKS Pod Identity are two AWS mechanisms that let Kubernetes pods assume IAM roles without embedding static credentials. Think of them like valet keys for your car — they work temporarily, have limited permissions, and automatically expire after use.

Here’s how they differ from the old approach:

Approach How Credentials Work Security Risk
Static Keys (Bad) AWS_ACCESS_KEY_ID in env vars or secrets Keys never expire, can leak via logs/containers
Node IAM Role (Better) All pods on a node share the same role Too permissive — one compromised pod = all permissions
IRSA (Best) Each pod gets its own temporary credentials via OIDC Scoped per pod, auto-rotated every hour
Pod Identity (Newest) Simplified IRSA with fewer moving parts Same security, easier setup (EKS 1.24+)

Key Benefits:

  • No credential rotation needed: Temporary credentials expire automatically every 60 minutes and refresh transparently.
  • Least privilege by design: Each pod gets only the IAM permissions it needs, not the entire node role.
  • Audit trail: Every AWS API call is tagged with the pod’s service account, making CloudTrail logs useful.
  • Zero config in app code: Your application uses the AWS SDK normally — no special credential handling required.
  • Works with third-party tools: Fluent Bit, external-dns, ALB Ingress Controller — anything that calls AWS APIs.

What Most Tutorials Get Wrong: Many guides treat IRSA and Pod Identity as interchangeable. They’re not. IRSA has been around since 2019 and requires an OIDC provider setup. Pod Identity (GA in late 2023) is AWS’s simpler alternative that skips OIDC entirely. If you’re starting fresh on EKS 1.24+, use Pod Identity. If you already have IRSA working, don’t rush to migrate — it’s not deprecated.

How IRSA Works Under the Hood

IRSA uses OpenID Connect (OIDC) to bridge Kubernetes identities to AWS IAM. Here’s the flow:

  1. EKS creates an OIDC provider in your AWS account tied to your cluster.
  2. You create an IAM role and configure it to trust your cluster’s OIDC provider.
  3. You annotate a Kubernetes ServiceAccount with the IAM role ARN.
  4. When a pod uses that ServiceAccount, EKS injects a web identity token into the pod.
  5. The AWS SDK exchanges that token for temporary IAM credentials via STS AssumeRoleWithWebIdentity.

The pod never sees the credentials directly — they’re fetched on-demand and cached by the SDK.

Setting Up IRSA (EKS 1.23–1.30)

Step 1: Create the OIDC Provider (one-time per cluster)

# Get your cluster's OIDC issuer URL
OIDC_URL=$(aws eks describe-cluster \
  --name my-cluster \
  --region us-west-2 \
  --query "cluster.identity.oidc.issuer" \
  --output text)

# Extract the ID (last part after https://oidc.eks...)
OIDC_ID=$(echo $OIDC_URL | awk -F'/' '{print $NF}')

# Create OIDC provider if it doesn't exist
aws iam create-open-id-connect-provider \
  --url $OIDC_URL \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list "9e99a48a9960b14926bb7f3b02e22da2b0ab7280"

That thumbprint is static for all EKS clusters — it’s the AWS root CA thumbprint.

Step 2: Create an IAM Role with Trust Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/${OIDC_ID}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-west-2.amazonaws.com/id/${OIDC_ID}:sub": "system:serviceaccount:my-namespace:my-service-account",
          "oidc.eks.us-west-2.amazonaws.com/id/${OIDC_ID}:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

The Condition block is critical — it restricts the role to one specific ServiceAccount in one specific namespace. Change the namespace or ServiceAccount name, and the trust breaks.

Step 3: Attach Permissions to the Role

# Inline policy example (S3 read-only for a specific bucket)
aws iam put-role-policy \
  --role-name my-pod-role \
  --policy-name s3-read-access \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-data-bucket",
        "arn:aws:s3:::my-data-bucket/*"
      ]
    }]
  }'

Step 4: Annotate the Kubernetes ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-service-account
  namespace: my-namespace
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-pod-role

Apply this with kubectl apply -f serviceaccount.yaml.

Step 5: Use the ServiceAccount in Your Pod

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: my-namespace
spec:
  serviceAccountName: my-service-account  # <-- This line is key
  containers:
  - name: app
    image: my-app:latest
    env:
    - name: AWS_REGION
      value: "us-west-2"

That’s it. Your pod can now call AWS APIs with the permissions defined in my-pod-role.

How Pod Identity Works (Simpler Alternative)

Pod Identity skips the OIDC provider entirely. AWS manages the identity plumbing behind the scenes using a new component called the EKS Pod Identity Agent (runs as a DaemonSet on every node).

Setting Up Pod Identity (EKS 1.24+)

Step 1: Enable Pod Identity on Your Cluster

# Install the Pod Identity Agent addon (one-time per cluster)
aws eks create-addon \
  --cluster-name my-cluster \
  --addon-name eks-pod-identity-agent \
  --region us-west-2

This deploys a DaemonSet that intercepts AWS API calls from pods and injects temporary credentials.

Step 2: Create an IAM Role (Standard Trust Policy)

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Service": "pods.eks.amazonaws.com"
    },
    "Action": [
      "sts:AssumeRole",
      "sts:TagSession"
    ]
  }]
}

Notice the trust policy is simpler — no OIDC provider ARN needed.

Step 3: Create a Pod Identity Association

aws eks create-pod-identity-association \
  --cluster-name my-cluster \
  --namespace my-namespace \
  --service-account my-service-account \
  --role-arn arn:aws:iam::123456789012:role/my-pod-role \
  --region us-west-2

This creates a mapping between your ServiceAccount and IAM role. No annotations needed on the ServiceAccount itself.

Step 4: Use the ServiceAccount in Your Pod (Same as IRSA)

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: my-namespace
spec:
  serviceAccountName: my-service-account
  containers:
  - name: app
    image: my-app:latest

The Pod Identity Agent automatically provides credentials to any pod using that ServiceAccount.

Real-World Example: Logging to S3 with Fluent Bit

Let’s set up Fluent Bit to ship logs to S3 without hardcoded credentials. We’ll use Pod Identity since it’s cleaner.

1. Create the IAM Role and Policy

# Create trust policy file
cat > trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "pods.eks.amazonaws.com"},
    "Action": ["sts:AssumeRole", "sts:TagSession"]
  }]
}
EOF

# Create the role
aws iam create-role \
  --role-name fluent-bit-s3-role \
  --assume-role-policy-document file://trust-policy.json

# Attach S3 write permissions
aws iam attach-role-policy \
  --role-name fluent-bit-s3-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess  # Tighten this in production

2. Create the ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: logging

3. Create Pod Identity Association

aws eks create-pod-identity-association \
  --cluster-name production-cluster \
  --namespace logging \
  --service-account fluent-bit \
  --role-arn arn:aws:iam::123456789012:role/fluent-bit-s3-role \
  --region us-west-2

4. Deploy Fluent Bit with the ServiceAccount

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: logging
spec:
  selector:
    matchLabels:
      app: fluent-bit
  template:
    metadata:
      labels:
        app: fluent-bit
    spec:
      serviceAccountName: fluent-bit  # Fluent Bit gets S3 permissions here
      containers:
      - name: fluent-bit
        image: fluent/fluent-bit:2.0
        env:
        - name: AWS_REGION
          value: us-west-2
        volumeMounts:
        - name: config
          mountPath: /fluent-bit/etc/
      volumes:
      - name: config
        configMap:
          name: fluent-bit-config

Fluent Bit will now write logs to S3 using temporary credentials fetched from the Pod Identity Agent. No AWS keys anywhere in your cluster.

Best Practices

  1. One ServiceAccount per workload with distinct permissions — Don’t share a single SA across unrelated apps just because they need similar AWS access. Create separate SAs and roles so you can audit and revoke permissions independently. If your API server and cron jobs both need S3 access, give them different ServiceAccounts tied to different roles.

  2. Use session tags for attribution in CloudTrail — When creating Pod Identity associations, add session tags like team, environment, and app-name. These appear in CloudTrail logs and help track which pod made which API call during an incident. With IRSA, you get the service account name automatically in the sourceIdentity field.

  3. Scope IAM policies tightly to resource ARNs — Never use Resource: "*" in IAM policies. Pin permissions to specific S3 buckets, DynamoDB tables, or SQS queues. If a pod only reads from s3://logs-prod-usw2/app-logs/, don’t give it access to s3://logs-*/*. Wildcards are audit failures waiting to happen.

  4. Rotate roles, not credentials — Since temporary credentials expire every 60 minutes, your security posture improves without manual rotation. But if a role is compromised, you still need to delete the Pod Identity association or IRSA annotation, then recreate the role with a new ARN. Automate this with Terraform or CloudFormation.

  5. Monitor failed AssumeRole attempts in CloudTrail — Set up an alarm for sts:AssumeRoleWithWebIdentity errors. These indicate misconfigured trust policies, deleted ServiceAccounts, or pods trying to assume roles they shouldn’t have. Most breaches start with reconnaissance via failed AWS API calls.

  6. Use eksctl for IRSA if you’re not automating with IaC — The eksctl CLI has a create iamserviceaccount command that sets up the OIDC provider, creates the IAM role, and annotates the ServiceAccount in one shot. It’s faster than manual AWS CLI steps and harder to misconfigure. For Pod Identity, there’s no shortcut — use the AWS CLI or Terraform.

Conclusion

IRSA and Pod Identity eliminate the security nightmare of static AWS credentials in Kubernetes. IRSA has been battle-tested for years and works everywhere. Pod Identity is newer but simpler — if you’re on EKS 1.24+, it’s the better choice for new projects. Both give you pod-level IAM permissions that auto-rotate and leave clean audit trails.

Key Takeaways:

  • IRSA uses OIDC federation; Pod Identity uses an EKS-managed agent. Both achieve the same result — temporary, scoped credentials.
  • Pod Identity requires EKS 1.24+ but needs no OIDC provider setup. IRSA works on older clusters but has more moving parts.
  • Always scope IAM policies to specific resources, not *. Generic permissions defeat the purpose of pod-level isolation.
  • Session tags and CloudTrail monitoring are critical for tracking which pod made which AWS call during incidents.
  • For legacy clusters, stick with IRSA. For greenfield EKS deployments, use Pod Identity.

Start by migrating your highest-risk workloads first — anything touching S3, Secrets Manager, or DynamoDB. Once you see how much cleaner the setup is compared to static keys, you’ll wonder why you didn’t do this years ago.

Frequently Asked Questions

Q: Can I use IRSA and Pod Identity on the same cluster?
Yes, but don’t mix them for the same workload. If you have legacy apps using IRSA, leave them alone. Use Pod Identity for new deployments. EKS handles both mechanisms in parallel without conflicts.

Q: What happens if my pod needs access to multiple AWS services?
Create a single IAM role with policies for all required services and attach it to one ServiceAccount. Don’t create multiple ServiceAccounts per pod — Kubernetes only allows one ServiceAccount per pod spec.

Q: Do IRSA credentials work outside EKS (like in local development)?
No. IRSA and Pod Identity only work inside EKS because they rely on EKS injecting the web identity token or the Pod Identity Agent. For local dev, use AWS profiles or environment variables with short-lived credentials from aws sts assume-role.

Q: How do I troubleshoot “AccessDenied” errors with IRSA?
Check three things: (1) Is the ServiceAccount annotated correctly with the role ARN? (2) Does the IAM role’s trust policy match the exact namespace and ServiceAccount name? (3) Are the IAM permissions broad enough for the API call? Use kubectl describe pod to verify the token is injected at /var/run/secrets/eks.amazonaws.com/serviceaccount/.

Q: Can I revoke a pod’s AWS access without restarting it?
With Pod Identity, delete the association — the next time the pod requests credentials, it’ll be denied. With IRSA, remove the ServiceAccount annotation and delete the pod (credentials are cached for up to an hour in the pod). For immediate revocation, add an explicit Deny policy to the IAM role targeting the pod’s session name.