This issue has been assessed as high severity. Review affected configurations immediately.
The EC2 Instance Metadata Service (IMDS) is a local HTTP endpoint available to every EC2 instance at the link-local address 169.254.169.254. It provides instance identity documents, user data, and — critically — the temporary IAM role credentials the instance uses to call AWS APIs. In the original IMDSv1 design, this endpoint requires no authentication: any HTTP request from any process on the instance receives a response.
When a web application running on that instance has a Server-Side Request Forgery (SSRF) vulnerability, an external attacker can route requests through the application to the metadata endpoint and retrieve IAM credentials without ever gaining OS-level access. This has been the root cause of several high-profile cloud breaches, most notably the 2019 Capital One incident in which a misconfigured WAF on an EC2 instance allowed an attacker to retrieve IAM role credentials via SSRF, ultimately leading to the exfiltration of over 100 million customer records.
The Exploitation Chain
Step 1: Discover SSRF
The attacker finds a parameter that causes the server-side application to make an outbound HTTP request — a URL fetch, a webhook, an image import, a PDF renderer, or an XML parser that resolves external entities. Any of these can be redirected to the metadata IP.
# Attacker-supplied URL in a vulnerable parameter
https://vulnerable-app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Step 2: Enumerate the IAM Role Name
The metadata endpoint returns available endpoints as plain text. The first request reveals the role name attached to the instance:
# Response from metadata endpoint
my-ec2-app-role
Step 3: Retrieve Temporary Credentials
A second request appending the role name returns a JSON object containing a time-limited access key, secret key, and session token:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-app-role
{
"Code": "Success",
"LastUpdated": "2026-06-05T07:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "IQoJb3JpZ2luX2VjEA...",
"Expiration": "2026-06-05T13:00:00Z"
}
Step 4: Use Credentials from an External IP
The attacker configures the AWS CLI with the stolen credentials and begins enumeration from their own machine — entirely outside the AWS environment:
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEA..."
export AWS_DEFAULT_REGION="us-east-1"
# Enumerate what this role can do
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name my-ec2-app-role
aws s3 ls
aws secretsmanager list-secrets
The blast radius at this point is entirely determined by the IAM role’s permissions — which are frequently over-provisioned relative to the application’s actual requirements.
Why IMDSv2 Eliminates This Attack
IMDSv2 adds a mandatory token exchange before credential retrieval. The token request requires a PUT method with a specific X-aws-ec2-metadata-token-ttl-seconds header:
# Step 1: Get a session token (requires PUT — SSRF is typically GET/POST)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Step 2: Use token for metadata requests
curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-H "X-aws-ec2-metadata-token: $TOKEN"
The PUT requirement is the critical protection. Most SSRF vulnerabilities operate via GET — they can follow redirects and fetch URLs, but cannot issue PUT requests with custom headers. A well-implemented SSRF that cannot issue PUT requests cannot obtain a valid token, and therefore cannot retrieve credentials via the metadata service.
IMDSv2 also defaults to a single-hop TTL on the token, which blocks scenarios where an attacker tries to relay the token through intermediate services.
Enforcing IMDSv2 at Scale
Option 1: Service Control Policy (Recommended)
AWS Organizations SCPs can block any instance launch that configures IMDSv1. Apply this to all OUs to prevent any account from launching IMDSv1-enabled instances:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireIMDSv2OnLaunch",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
},
{
"Sid": "DenyIMDSv1ModifyOnExistingInstances",
"Effect": "Deny",
"Action": "ec2:ModifyInstanceMetadataOptions",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:MetadataHttpTokens": "optional"
}
}
}
]
}
Option 2: Enforce IMDSv2 on Existing Instances
For instances already running, use the AWS CLI to flip all instances in a region to IMDSv2-required:
# List all instances still using IMDSv1 (HttpTokens = optional)
aws ec2 describe-instances \
--query "Reservations[*].Instances[?MetadataOptions.HttpTokens=='optional'].[InstanceId,Tags[?Key=='Name'].Value|[0]]" \
--output table
# Enforce IMDSv2 on a specific instance
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-endpoint enabled
# Bulk enforce in a region (pipe through xargs for all instances)
aws ec2 describe-instances \
--query "Reservations[*].Instances[?MetadataOptions.HttpTokens=='optional'].InstanceId" \
--output text | tr '\t' '\n' | \
xargs -I{} aws ec2 modify-instance-metadata-options \
--instance-id {} \
--http-tokens required \
--http-endpoint enabled
Option 3: Terraform — Enforce IMDSv2 in IaC
In any Terraform aws_instance resource, explicitly configure IMDSv2:
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = "t3.medium"
metadata_options {
http_tokens = "required" # Enforces IMDSv2
http_endpoint = "enabled"
http_hop_limit = 1 # Prevents relay through containers
}
# ... other configuration
}
Set http_hop_limit = 1 to prevent a container running inside the EC2 instance from forwarding metadata requests — relevant in mixed EC2/container environments.
Detecting IMDSv1 Credential Access via CloudTrail
When IMDSv1 credentials are used from outside the instance’s VPC, the sourceIPAddress in CloudTrail will be an external IP rather than the instance’s private IP. Alert on API calls where:
- The access key prefix begins with
ASIA(temporary credentials via STS AssumeRole) - The
sourceIPAddressis not in your known AWS IP ranges for that region - The caller identity matches an EC2 instance role
# CloudTrail Insights query — API calls from EC2 role credentials originating outside expected VPC CIDR
aws logs filter-log-events \
--log-group-name "aws-cloudtrail-logs" \
--filter-pattern '{ ($.userIdentity.type = "AssumedRole") && ($.userIdentity.sessionContext.sessionIssuer.type = "Role") && ($.sourceIPAddress != "10.*") && ($.sourceIPAddress != "172.16.*") && ($.sourceIPAddress != "192.168.*") }'
In Sentinel (KQL), correlate AWSCloudTrail table entries where UserIdentityType == "AssumedRole" and the SourceIpAddress is not in your private CIDR ranges, joined against known instance profile role ARNs.
AWS Config Rule for Continuous Compliance
AWS Config’s managed rule ec2-imdsv2-check continuously evaluates whether instances have IMDSv2 required. Enable it across all accounts:
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "ec2-imdsv2-check",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "EC2_IMDSV2_CHECK"
}
}'
Non-compliant instances trigger a Config finding that can feed Security Hub for centralised visibility.
Priority Remediation Order
- Apply the SCP to prevent new IMDSv1 instances — this is immediate and has no operational impact
- Inventory existing IMDSv1 instances using the CLI query above
- Test applications for IMDS dependency before enforcing — applications using instance metadata (retrieving region, instance ID, role credentials) will continue to work with IMDSv2; only applications using custom metadata tokens in unusual ways may need adjustment
- Enforce IMDSv2 on all instances within 30 days, prioritising internet-facing instances first