The problem IRSA solves
By default, a pod on an EKS node can reach the instance metadata service and obtain the credentials of the node’s IAM role. That role usually exists to let the node pull images and join the cluster, and over time it collects extras: S3 read access added for one team, a queue permission for another. Every pod on that node inherits all of it.
That is the wrong shape. If a single service has a remote code execution bug, the attacker gets whatever the node can do. This page describes how we fix that as part of AWS EKS consulting, and it is a core control in our wider EKS security work.
What good looks like
- No application pod relies on the node role for AWS access.
- Each workload that calls AWS has its own role, named and tagged for its owner.
- Permissions name specific resources, not wildcards, and use conditions where helpful.
- Trust policies pin one namespace and service account.
- Roles are code, reviewed in pull requests, and checked regularly for unused permissions.
How IRSA works
The cluster has an OIDC identity provider registered in IAM. When a pod uses a service account annotated with a role ARN, the EKS pod identity webhook injects a projected service account token and environment variables. The AWS SDK exchanges that token with STS using AssumeRoleWithWebIdentity and receives short-lived credentials for the role.
The security of the whole scheme rests on the trust policy of the role. A careful one looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:payments:invoice-worker",
"oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}
]
}
The account ID and provider ID above are illustrative. The sub condition is the important line: if it is missing or uses a wildcard, any service account in the cluster may assume the role. We audit for this in every engagement.
The service account then links to the role:
apiVersion: v1
kind: ServiceAccount
metadata:
name: invoice-worker
namespace: payments
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/payments-invoice-worker
IRSA or EKS Pod Identity
EKS Pod Identity is the newer mechanism. An agent add-on runs on nodes, and you associate a role with a service account through the EKS API instead of editing OIDC trust policies. Roles use a fixed trusted service principal, which makes reuse across clusters simpler and avoids the per-cluster OIDC provider.
| Factor | IRSA | EKS Pod Identity |
|---|---|---|
| Works outside EKS | Yes, with OIDC | EKS only |
| Trust policy | Cluster specific OIDC | Shared service principal |
| Setup | OIDC provider and annotations | Add-on and associations |
| Fargate support | Yes | Check current support for your case |
We often use Pod Identity for new EKS-only clusters and keep IRSA where clusters already use it or where portability matters. Mixing both in one cluster is possible and sometimes the pragmatic path during migration. Confirm current feature support in AWS documentation before committing.
Deriving least privilege
Writing the minimum policy is where effort goes. Our method:
- Read the application code and configuration to list intended AWS calls.
- Run it in a test cluster with a broader role and inspect CloudTrail for the actions actually used.
- Use IAM Access Analyzer policy generation as a starting point, never as the final answer.
- Restrict by resource ARN, add conditions such as required tags or source VPC endpoints, and remove wildcards.
- Test that unrelated actions are denied.
Each role is defined in Terraform alongside the workload’s other infrastructure. See Terraform modules and state for how these layers are organised.
Locking down the node role
IRSA only helps if pods cannot fall back to the node role. We set the instance metadata options to require IMDSv2 with a hop limit of 1 on worker nodes, which stops regular pods from reaching the metadata service. Pods that need host networking are reviewed individually. Pairing this with network policies and security groups closes more paths.
A worked example: a queue consumer
Take a worker that reads messages from one SQS queue and writes results to one S3 prefix. A first draft often arrives as a broad managed policy for each service. The least-privilege version names the single queue ARN with receive and delete actions, and the single bucket prefix with put and get actions. If the queue is encrypted with a customer managed key, a narrow decrypt permission on that key is added too.
Then we prove it. We run the worker in a test cluster, confirm it works, and attempt a call it should not be able to make, such as listing every bucket. That call must fail with an access denied error. Keeping that failing test in the repository means a future permission change that quietly widens access is caught in review.
Reviewing roles over time
Roles that were tight on day one tend to widen. We schedule a periodic review of last-accessed data and Access Analyzer findings for each workload role, remove actions that have not been used, and check that every role still has a named owner. Decommissioned services should have their roles deleted along with their service accounts, so nothing is left assumable by a name someone might reuse.
Common failure modes
- Wildcard trust policies, covered above.
- Shared roles across many services, which defeats the isolation. One role per service account is the rule.
- Add-on roles forgotten, leaving the CSI driver or load balancer controller on the node role.
- Role drift when permissions are widened during an incident and never reversed. Terraform plans catch this.
- Token audience mismatches when using custom SDK setups or older SDK versions that do not support web identity credentials.
Delivery and handover
You receive Terraform for the OIDC provider or Pod Identity setup, one role per workload, tests that prove denied actions fail, and a short guide for adding a role for a new service. We also give your developers a template so new workloads follow the pattern by default. Related controls such as KMS secrets encryption use these same roles to grant key access.
How to tell it is working
Look at CloudTrail: calls from your pods should appear under per-workload role sessions, not under the node instance role. IAM Access Analyzer should show no unused-permission findings that you cannot explain. If you would like us to review your current roles, contact us.