Skip to content

I'm currently populating my catalog on the site. Pardon the prefilled data. The entries are actively being updated and cleaned up.

Previous website

Understanding IRSA in EKS: How Pods Securely Access AWS Resources

A student-friendly guide to IRSA in EKS, explaining why node roles are not enough, how OIDC trust works, and how pods get short-lived AWS credentials safely.

2026-04-2112 min read
  • AWS
  • Kubernetes
  • Platform Engineering

Why IRSA matters#

One of the first confusing questions in EKS is this:

How does a pod talk to AWS without storing AWS keys inside the app?

That question shows up fast.

A pod may need to:

  • read from S3
  • fetch secrets from AWS Secrets Manager
  • write to DynamoDB
  • manage AWS infrastructure through a controller

At first, many people assume the answer is one of these:

  • put AWS access keys in a secret
  • let every pod use the worker node IAM role

Both options are weak.

  • Static keys are hard to rotate and easy to leak.
  • Node IAM roles are too broad when many workloads share the same node.

IRSA exists to solve that cleanly.

Plain-English definition

IRSA lets a Kubernetes service account assume a specific IAM role, so a pod can get temporary AWS credentials without storing long-lived keys.

The problem before IRSA#

Without IRSA, many teams end up relying on the IAM role attached to the EC2 worker nodes.

That means the node becomes the identity boundary.

So if three different applications run on the same node:

  • app A might need S3 read access
  • app B might need DynamoDB write access
  • app C might need no AWS access at all

But if they all depend on the node role, they can all inherit the same permission set.

That creates real problems:

  • least privilege becomes harder
  • service isolation becomes weaker
  • one compromised workload can get more AWS access than it should

This is the key idea:

Node identity is too coarse for workload-level security.

What IRSA actually is#

IRSA means IAM Roles for Service Accounts.

It connects two worlds:

  • Kubernetes decides who the workload is
  • AWS decides what that workload is allowed to do

The bridge between them is a trusted OIDC identity flow.

IRSA in one line
Kubernetes Service Account -> IAM Role -> Temporary AWS Credentials -> AWS APIs

If you remember only one sentence, remember this:

A pod gets AWS access because it runs as a service account that is allowed to assume an IAM role.

The building blocks#

IRSA feels easier once you separate the parts.

1. Kubernetes service account#

This is the workload identity inside the cluster.

Example:

YAML
apiVersion: v1kind: ServiceAccountmetadata:  name: aws-load-balancer-controller  namespace: kube-system

This says:

This workload runs as system:serviceaccount:kube-system:aws-load-balancer-controller.

That full string matters later.

2. OIDC provider#

AWS needs a way to trust identity tokens issued by the cluster.

That trust is created by registering the EKS cluster's OIDC issuer in IAM.

HCL
data "tls_certificate" "eks_oidc" {  url = var.oidc_issuer_url} resource "aws_iam_openid_connect_provider" "eks" {  url = var.oidc_issuer_url   client_id_list = ["sts.amazonaws.com"]   thumbprint_list = [    data.tls_certificate.eks_oidc.certificates[0].sha1_fingerprint  ]}

What AWS is being told here is:

I trust tokens issued by this cluster's OIDC endpoint for web identity federation.

Without this, AWS STS will not trust the pod's identity token.

3. IAM permissions policy#

This defines what the workload can do after it gets credentials.

HCL
resource "aws_iam_policy" "alb_controller" {  name   = "alb-controller-policy"  policy = file("aws-load-balancer-controller-policy.json")}

This is the permissions side:

  • allowed AWS actions
  • allowed resources
  • explicit limits on access

4. IAM trust policy#

This defines who is allowed to assume the role.

This is the most important part of IRSA.

HCL
data "aws_iam_policy_document" "assume_role" {  statement {    actions = ["sts:AssumeRoleWithWebIdentity"]     principals {      type        = "Federated"      identifiers = [aws_iam_openid_connect_provider.eks.arn]    }     condition {      test     = "StringEquals"      variable = "<OIDC_URL>:aud"      values   = ["sts.amazonaws.com"]    }     condition {      test     = "StringEquals"      variable = "<OIDC_URL>:sub"      values = [        "system:serviceaccount:kube-system:aws-load-balancer-controller"      ]    }  }}

This trust policy says:

  • the token must come from the trusted OIDC provider
  • the audience must be sts.amazonaws.com
  • the subject must be this exact service account in this exact namespace

That final sub check is where the strongest boundary lives.

Most important IRSA rule

If the namespace or service account name in the trust policy does not match exactly, the pod will not be able to assume the IAM role.

5. IAM role#

Now combine trust and permissions.

HCL
resource "aws_iam_role" "alb_controller" {  assume_role_policy = data.aws_iam_policy_document.assume_role.json} resource "aws_iam_role_policy_attachment" "attach" {  role       = aws_iam_role.alb_controller.name  policy_arn = aws_iam_policy.alb_controller.arn}

This gives us:

  • trust policy = who can use the role
  • permission policy = what they can do with the role

6. Service account annotation#

Finally, the Kubernetes service account is annotated with the IAM role ARN.

YAML
apiVersion: v1kind: ServiceAccountmetadata:  name: aws-load-balancer-controller  namespace: kube-system  annotations:    eks.amazonaws.com/role-arn: <IAM_ROLE_ARN>

This annotation is the handoff point.

It tells EKS:

Pods using this service account should assume this IAM role.

The full flow, step by step#

At a high level, the runtime path looks like this:

Runtime identity flow
Pod -> Service Account -> OIDC Token -> AWS STS -> IAM Role -> Temporary Credentials -> AWS API

Here is what that means in plain language:

  1. A pod starts and runs with a specific Kubernetes service account.
  2. Kubernetes makes a signed service account token available to that pod.
  3. The workload, or an AWS SDK inside it, uses that token to call AssumeRoleWithWebIdentity.
  4. AWS STS validates the token against the registered OIDC provider and the role trust policy.
  5. If everything matches, AWS returns temporary credentials.
  6. The pod uses those short-lived credentials to call AWS APIs.

That is why IRSA is safer than storing access keys in secrets.

The pod does not hold permanent credentials. It receives temporary credentials only after proving its identity.

A simple mental model#

When people struggle with IRSA, it is usually because they mix up identity and permissions.

Keep them separate:

IRSA mental model
Service Account = Kubernetes identityIAM Role        = AWS permission setOIDC            = Trust bridgeSTS             = Credential exchange

If you understand those four lines, you already understand most of IRSA.

Real example: AWS Load Balancer Controller#

The AWS Load Balancer Controller is one of the easiest ways to see IRSA in practice.

The controller needs AWS permissions because it creates and manages things like:

  • Application Load Balancers
  • listeners
  • target groups
  • target registration
  • related networking resources

But those permissions should belong to the controller workload only, not to every pod on the node.

That makes it a strong IRSA use case.

Example Helm integration#

In many setups, the service account annotation is wired through Helm.

HCL
resource "helm_release" "alb_controller" {  name      = "aws-load-balancer-controller"  namespace = "kube-system"   set = [    {      name  = "serviceAccount.create"      value = "true"    },    {      name  = "serviceAccount.name"      value = "aws-load-balancer-controller"    },    {      name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"      value = module.alb_controller_irsa.alb_controller_role_arn    }  ]}

That makes the flow operational:

  • Helm creates the service account
  • the IRSA annotation is attached
  • the controller pods use that service account
  • the controller gets only the AWS permissions it actually needs

Why this is better than node roles#

IRSA improves a cluster in a few important ways.

Better least privilege#

Each workload can get a role with a permission scope that matches its job.

That means:

  • a controller can manage load balancers
  • a reporting app can read from S3
  • a payment service can access only the secrets it needs

Not every workload has to inherit the same broad AWS access.

Smaller blast radius#

If one pod is compromised, the attacker gets only the permissions attached to that workload's role.

That is far better than inheriting a powerful node role shared by unrelated applications.

No long-lived AWS keys in manifests#

IRSA avoids a very common bad pattern:

  • creating IAM users
  • generating access keys
  • storing those keys in Kubernetes secrets

Temporary credentials are safer and easier to reason about.

Where beginners usually get confused#

These are the mistakes that break IRSA most often.

The trust policy subject does not match#

If your trust policy says:

TXT
system:serviceaccount:kube-system:aws-load-balancer-controller

but your real service account is in another namespace or uses another name, role assumption fails.

The match must be exact.

The OIDC provider is missing#

If the cluster issuer was never registered in IAM, AWS has no trusted identity provider to validate against.

The annotation is missing#

If the service account does not include:

YAML
eks.amazonaws.com/role-arn: <IAM_ROLE_ARN>

the pod will not know which role to assume.

You are still thinking in node terms#

A lot of confusion disappears when you stop asking:

What permissions does this node have?

and start asking:

What permissions should this workload have?

That is the IRSA mindset.

A quick student-friendly checklist#

If you want to verify an IRSA setup, check these in order:

  1. Does the EKS cluster have a registered OIDC provider in AWS IAM?
  2. Does the IAM role trust the correct OIDC provider?
  3. Does the trust policy sub match the exact namespace and service account?
  4. Does the service account include the correct eks.amazonaws.com/role-arn annotation?
  5. Does the attached IAM policy allow the AWS actions the workload needs?
  6. Is the pod actually using that service account?

That short checklist catches a lot of real-world misconfigurations.

Final takeaway#

IRSA is not just an AWS feature to memorize.

It is the security model that gives Kubernetes workloads a safe way to prove identity to AWS and receive short-lived permissions.

Once that clicks, EKS becomes much easier to reason about:

  • Kubernetes provides the workload identity
  • AWS IAM defines the permissions
  • OIDC establishes trust
  • STS exchanges identity for temporary credentials

That is why IRSA matters so much in production EKS environments.

Closing note#

If you are learning EKS, IRSA is worth slowing down for.

It explains how controllers safely talk to AWS. It explains how application pods reach S3 or Secrets Manager. And it explains why modern cloud-native security is built around identity plus short-lived credentials instead of static secrets.

Related Posts

Additional notes connected to the same operating and platform engineering themes.

Kubernetes Operators: The Missing Piece After Helm

A practical first-principles guide to Kubernetes Operators, why Helm only solves Day 1 deployment, and how operators encode Day 2 operational knowledge into the cluster.

  • Kubernetes
  • Operators
  • Helm
  • Platform Engineering

Helm Explained Properly: Why Kubernetes Needed It and How It Makes Life Easier

A practical guide to what Helm solves in Kubernetes, how charts and values work, and why releases, upgrades, and rollbacks make real operations easier.

  • Kubernetes
  • Helm
  • CI/CD

Kubernetes Internals Notes: API Server, RBAC, Scheduling, and Controllers

A practical, student-friendly guide to the Kubernetes request flow, authentication vs authorization, controllers, scheduler behavior, rolling updates, and workload resilience.

  • Kubernetes
  • Platform Engineering
  • AWS