This issue has been assessed as critical severity. Review affected configurations immediately.
The Problem
Every EC2 instance backing an EKS node group has an IAM role — the node instance profile. This role is designed to give the Kubelet the permissions it needs to register with the cluster, pull images from ECR, and manage EBS volumes. In practice, these roles frequently accumulate permissions far beyond that scope, and they’re accessible to every workload running on the node.
The attack path is well-documented but consistently underestimated:
- Attacker achieves code execution in a pod (via compromised container image, SSRF, or application vulnerability)
- Pod queries the EC2 Instance Metadata Service (IMDS v1) at
169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> - Credentials returned are the node IAM role’s short-lived access keys
- Attacker uses those credentials to enumerate and pivot within AWS
This is not a Kubernetes vulnerability — it’s a misconfiguration in how node roles are scoped and how workloads are isolated from the metadata service.
Step 1: Enumerating the Attack Surface
What a pod can reach by default
In a default EKS cluster with IMDSv1 enabled and no network policy applied:
# From inside a pod — query instance metadata
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns the role name, then:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME>
The response returns AccessKeyId, SecretAccessKey, and Token — valid AWS credentials with the node role’s permissions, good for up to 6 hours.
Auditing your node role permissions
# Find your node role name
aws eks describe-nodegroup \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup \
--query 'nodegroup.nodeRole'
# List attached policies
aws iam list-attached-role-policies --role-name <NODE_ROLE>
# Check inline policies
aws iam list-role-policies --role-name <NODE_ROLE>
The minimum required permissions for an EKS worker node role:
AmazonEKSWorkerNodePolicyAmazonEC2ContainerRegistryReadOnlyAmazonEKS_CNI_Policy(or delegate to IRSA)
Any permission beyond these on the node role represents blast radius expansion for a compromised pod.
Step 2: What an Attacker Can Do With a Bloated Node Role
The damage depends on what’s attached to the node role. Common misconfigurations and their consequences:
ec2:DescribeInstances + ec2:RunInstances + iam:PassRole: The attacker can launch new EC2 instances with any role they can pass, potentially including highly privileged roles. This is full account compromise via a pod.
s3:GetObject on *: Every S3 object across the account is readable. This includes application configs, database backups, log archives, and any secrets stored in S3.
secretsmanager:GetSecretValue on *: All secrets across the account exposed.
sts:AssumeRole: If the node role can assume other roles (common in multi-account or CI/CD setups), the attacker pivots to any assumable role — potentially cross-account.
iam:CreateUser / iam:AttachUserPolicy: The attacker creates a persistent backdoor IAM user with admin privileges, surviving even if the pod is terminated.
Step 3: The Three Controls That Stop This
Control 1: Enforce IMDSv2 on all node groups
IMDSv2 requires a PUT request with a TTL-bound session token before GET requests are honoured. This blocks simple SSRF and single-step curl exploitation — the most common pod-level metadata access.
# Launch template metadata options
aws ec2 modify-instance-metadata-options \
--instance-id <id> \
--http-tokens required \
--http-put-response-hop-limit 1
# For EKS managed node groups, enforce in launch template:
# MetadataOptions:
# HttpTokens: required
# HttpPutResponseHopLimit: 1 ← hop limit 1 prevents containers from reaching IMDS
Setting the hop limit to 1 means only the node itself can reach IMDS — not pods, which require an additional network hop via the container network interface.
Control 2: Scope the node role to minimum permissions
Remove everything from the node role that isn’t required for node operation:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeRouteTables",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVolumes",
"ec2:DescribeAttachVolume",
"ec2:DescribeVpcs"
],
"Resource": "*"
}
]
}
Use IRSA (IAM Roles for Service Accounts) to grant pods the specific AWS permissions they need — not the node role.
Control 3: Use IRSA for all pod-level AWS access
IRSA assigns IAM roles to Kubernetes service accounts using OIDC federation. Each pod gets only the permissions explicitly granted to its service account.
# Enable OIDC provider for your cluster
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster --approve
# Create a pod-scoped IAM role
eksctl create iamserviceaccount \
--cluster my-cluster \
--namespace my-app \
--name my-app-sa \
--attach-policy-arn arn:aws:iam::ACCOUNT:policy/MyAppPolicy \
--approve
With IRSA, the pod receives temporary credentials scoped to its service account’s permissions — not the node role. Even if the pod is compromised, the credentials are narrow.
Detection
CloudTrail queries for credential abuse
# Calls using instance profile credentials from unexpected IP ranges
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<KEY_FROM_NODE_ROLE> \
--start-time 2026-06-01
# STS GetCallerIdentity calls — attacker enumeration step
aws logs filter-log-events \
--log-group-name /aws/cloudtrail/management-events \
--filter-pattern '{ $.eventName = "GetCallerIdentity" && $.userIdentity.type = "AssumedRole" }'
AWS GuardDuty findings to monitor
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS— credentials from an EC2 instance profile used from an external IP. High confidence indicator.Kubernetes:EKSClustersAccessedExternally— API server access from unusual sourcesDiscovery:S3/MaliciousIPCaller— S3 enumeration calls from known-malicious IPs
Kubernetes audit log (via CloudWatch)
# Enable control plane logging
aws eks update-cluster-config \
--name my-cluster \
--logging '{"clusterLogging":[{"types":["audit","api","authenticator"],"enabled":true}]}'
# Query for exec into pods (attacker privilege escalation)
aws logs filter-log-events \
--log-group-name /aws/eks/my-cluster/cluster \
--filter-pattern '{ $.verb = "create" && $.objectRef.subresource = "exec" }'
Terraform: Enforcing IMDSv2 at Scale
resource "aws_launch_template" "eks_nodes" {
name_prefix = "eks-node-"
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2 only
http_put_response_hop_limit = 1 # Block pod access
}
}
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "main"
node_role_arn = aws_iam_role.node_role.arn
launch_template {
id = aws_launch_template.eks_nodes.id
version = aws_launch_template.eks_nodes.latest_version
}
# ... subnet_ids, scaling_config etc
}
The node IAM role is the most direct path from a compromised pod to AWS account-level access. Enforce IMDSv2 with hop limit 1, strip the node role to minimum permissions, and move all application AWS permissions to IRSA service accounts. These three changes remove the escalation path without affecting any workload functionality.