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.
Kubernetes Service Account -> IAM Role -> Temporary AWS Credentials -> AWS APIsIf 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:
apiVersion: v1kind: ServiceAccountmetadata: name: aws-load-balancer-controller namespace: kube-systemThis 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.
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.
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.
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.
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.
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:
Pod -> Service Account -> OIDC Token -> AWS STS -> IAM Role -> Temporary Credentials -> AWS APIHere is what that means in plain language:
- A pod starts and runs with a specific Kubernetes service account.
- Kubernetes makes a signed service account token available to that pod.
- The workload, or an AWS SDK inside it, uses that token to call
AssumeRoleWithWebIdentity. - AWS STS validates the token against the registered OIDC provider and the role trust policy.
- If everything matches, AWS returns temporary credentials.
- 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:
Service Account = Kubernetes identityIAM Role = AWS permission setOIDC = Trust bridgeSTS = Credential exchangeIf 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.
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:
system:serviceaccount:kube-system:aws-load-balancer-controllerbut 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:
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:
- Does the EKS cluster have a registered OIDC provider in AWS IAM?
- Does the IAM role trust the correct OIDC provider?
- Does the trust policy
submatch the exact namespace and service account? - Does the service account include the correct
eks.amazonaws.com/role-arnannotation? - Does the attached IAM policy allow the AWS actions the workload needs?
- 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.
