AWS IAM in Kubernetes (EKS)

Introduction

The first time I ran a workload on Amazon EKS that needed to write to S3, I did what every tutorial told me to do back then: create an IAM user, generate an access key, store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in a Kubernetes Secret, and inject them as environment variables. It worked. It was also a ticking time bomb.

That approach tied long-lived credentials to a secret that could be read by anyone with kubectl get secret access in that namespace. Rotation was manual. When an engineer left the team, we weren't sure which access keys to revoke. When we discovered one key had leaked into a public repository, the post-mortem was painful.

The right approach is to use IAM Roles for Service Accounts (IRSA) or the newer EKS Pod Identity — both let Kubernetes Service Accounts assume IAM roles without any long-lived credentials. This article covers both mechanisms, when to use each, and how to operate them at scale.

Table of Contents


The Problem with Static AWS Credentials

spinner

Why Static Credentials Are Dangerous

Risk
Impact

Long-lived keys (never expire)

Single compromised key = persistent breach

Manual rotation

Teams skip rotations; stale keys accumulate

Overly broad permissions

"Just give it full S3 access" anti-pattern

Shared keys across services

One breach exposes all services

No attribution

CloudTrail shows "IAM user" not "service"


IAM Roles for Service Accounts (IRSA)

IRSA was introduced in EKS in 2019. It works by federating the EKS cluster's OIDC provider with AWS IAM, allowing Kubernetes Service Accounts to assume IAM roles using projected service account tokens — the same mechanism as AKS Workload Identity.

spinner

How It Works

  1. EKS cluster has an OIDC provider with a unique issuer URL

  2. Kubernetes injects a short-lived OIDC-compatible token into pods via projected volume

  3. The AWS SDK calls AssumeRoleWithWebIdentity using this token

  4. AWS STS validates the token against the cluster's OIDC provider

  5. STS returns temporary credentials scoped to the IAM role

  6. The SDK uses these credentials (auto-rotating before expiry)


IRSA Setup and Configuration

Prerequisites

Step 1: Create EKS Cluster with OIDC Enabled

Enable OIDC on an Existing Cluster

Step 2: Create an IAM Policy

Step 3: Create IAM Role with Trust Policy

The trust policy defines which Kubernetes Service Accounts can assume this role.

Step 4: Create Annotated Kubernetes Service Account

Or using eksctl for simple cases:

Step 5: Deploy Your Application

After deployment, verify the webhook-injected environment variables:


EKS Pod Identity (The Modern Approach)

Launched in late 2023, EKS Pod Identity is AWS's simplified alternative to IRSA. It removes the need to manage OIDC provider configurations per-cluster and simplifies the IAM trust policy.

spinner

Key Differences from IRSA

Aspect
IRSA
EKS Pod Identity

OIDC provider required

Yes, per cluster

No

Trust policy format

References OIDC + subject

References pods.eks.amazonaws.com

Association mechanism

KSA annotation

EKS API resource

Cross-account

Supported

Supported (via role chaining)

Node agent required

No

Yes (EKS Pod Identity Agent add-on)

Portability

Any EKS cluster with OIDC

EKS only


EKS Pod Identity Setup

Step 1: Enable EKS Pod Identity Agent Add-on

Step 2: Create an IAM Role with Pod Identity Trust Policy

The trust policy for Pod Identity is simpler — it trusts the EKS service principal, not a cluster-specific OIDC provider.

Step 3: Create Pod Identity Association

Instead of annotating the Service Account, you create an EKS API resource that links the KSA to the role.

Step 4: Create Kubernetes Service Account (No Annotation Needed)

Step 5: Deploy Your Application

The Pod Identity Agent DaemonSet intercepts credential requests, handles the STS exchange, and delivers credentials to pod containers automatically.


IRSA vs EKS Pod Identity Comparison

spinner

Detailed Comparison

Feature
IRSA
EKS Pod Identity

Maturity

GA since 2019, widely adopted

GA since 2024

OIDC provider

Required, per-cluster

Not required

Trust policy

References <oidc_provider>:sub

References pods.eks.amazonaws.com

Service account annotation

Required

Not required

Association management

Implicit (annotation)

Explicit (EKS API)

Node agent

None

Pod Identity Agent DaemonSet

Maximum sessions per role

No limit

500 per namespace/SA pair per role

Terraform support

Mature

Available via aws_eks_pod_identity_association

Cluster migration

Trust policy must be updated per cluster

No changes needed

EKS Anywhere / non-EKS

Supported

EKS only

Graviton2/ARM support

Yes

Yes


Application Code Integration

Go

Python

Node.js / TypeScript


IAM Policy Best Practices

Principle of Least Privilege

Condition Keys for IRSA

Use StringEquals not StringLike in trust policies. Wildcards (*) in the sub condition would allow any service account to assume the role.

Resource-Based Policies (S3 Bucket Policy)

Common Service-Specific Policies


Cross-Account Access

A common pattern is having workloads in an EKS cluster in Account A access resources in Account B.

spinner

Setup Cross-Account Access

In Account B — create a role that Account A can assume:

In Account A — grant the pod's role permission to assume the Account B role:

In application code — use STS to assume the cross-account role:


Multi-Cluster and Multi-Environment Patterns

Pattern: One Role Per Service Per Environment

Managing with Terraform


Security Best Practices

1. Never Use StringLike with Wildcards in Trust Policies

2. Enable CloudTrail for IAM Role Activity

3. Use AWS IAM Access Analyzer

4. Restrict the OIDC Audience

5. Set Maximum Session Duration Appropriately

6. Use AWS Secrets Manager Instead of Kubernetes Secrets for Sensitive Values


Troubleshooting

WebIdentityErr: Failed to Retrieve Credentials

AccessDenied When Calling AWS Service

IRSA: Role Not Being Assumed (No Annotation Effect)

EKS Pod Identity: Credentials Not Available

Validate IAM Permissions (Without Deploying)


What I Learned

After migrating multiple EKS clusters from static credentials to IRSA, and more recently to Pod Identity, here are the lessons I keep coming back to:

  1. Start with Pod Identity for new clusters. IRSA is battle-tested and widely supported in Terraform modules, but Pod Identity is simpler to manage at scale — no cluster-specific OIDC provider references in trust policies means you can reuse roles across clusters without updating policies.

  2. Always use StringEquals in trust policy conditions, never StringLike with wildcards. A wildcarded trust policy is effectively giving every workload in your cluster access to that role's permissions.

  3. One IAM role per service per environment. Shared roles mean shared blast radius. The overhead of maintaining separate roles pays for itself when an incident isolates to one service rather than cascading.

  4. LoadDefaultConfig / boto3.Session() just works. Don't implement custom credential providers. The AWS SDK credential chain handles IRSA, Pod Identity, instance profiles, and local ~/.aws/credentials transparently — same code runs everywhere.

  5. CloudTrail is your audit trail. Every AssumeRoleWithWebIdentity call shows the source OIDC token's subject (system:serviceaccount:<ns>:<name>). Enable CloudTrail from day one; it's invaluable for debugging AND compliance.

  6. ECR image pulls use the node instance profile, not IRSA. Don't annotate your service account with ECR permissions expecting it to help with image pulls — the kubelet on the node handles that via its own IAM role.

  7. Test your trust policy before deploying. aws iam simulate-principal-policy saves deployment debugging cycles. Paste your trust policy into the IAM Policy Simulator to confirm conditions work as expected.


Summary

spinner

Next steps:

Last updated