SSRF to cloud keys, and why IMDSv2 exists
How a single "fetch this URL" feature turns into stolen IAM credentials, what IMDSv2 actually changes at the HTTP level, and the layered defences I recommend after every cloud-hosted VAPT.
Server-side request forgery is the bug class I find most often in features that sound harmless: link previews, PDF and screenshot renderers, webhook testers, "import from URL", image proxies. Any time the server fetches a URL the user influences, I test it — because in the cloud, an SSRF is frequently one request away from credentials.
The magic address: 169.254.169.254
Every major cloud exposes an instance metadata service on a link-local address reachable only from inside the VM. On AWS EC2, the original version (IMDSv1) is a plain HTTP GET:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# -> my-app-role
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/my-app-role
# -> { "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", ... }
Those are temporary credentials for whatever IAM role the instance has. If your PDF renderer will fetch http://169.254.169.254/... on an attacker's behalf, the attacker now is your application in AWS.
This isn't theoretical. The 2019 Capital One breach involved exactly this chain: a misconfigured web application firewall was abused to make requests to the metadata service, and the retrieved role credentials were used to access S3 data.
What IMDSv2 changes
AWS introduced IMDSv2 in November 2019. It's session-oriented:
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/
Three properties make this much harder to reach through SSRF:
- A
PUTis required to obtain a token. Most SSRF primitives can only issueGETs. - A custom header is required on every read. Most SSRF primitives can't set arbitrary headers.
- The token response has an IP hop limit (default 1), so it doesn't survive extra network hops — for example out of a container through a forwarding layer. Teams running containers on the host often raise it to 2; that's a deliberate trade-off, not a default to copy blindly.
Crucially, IMDSv2 only protects you if v1 is turned off. Set HttpTokens=required on instances and launch templates; otherwise the old GET path still works.
Other clouds use a similar idea: GCP's metadata server requires the Metadata-Flavor: Google header and Azure IMDS requires Metadata: true — again, headers a basic SSRF usually can't add.
How I test for SSRF
In an assessment I work through roughly this list:
- Direct targets:
169.254.169.254,metadata.google.internal,localhost, internal hostnames and RFC 1918 ranges, plus admin ports (Redis 6379, Elasticsearch 9200, Kubernetes API, Docker socket proxies). - Encoding tricks: decimal (
2852039166), octal and IPv6 forms ([::ffff:169.254.169.254]) to get past naive blocklists. - Redirects: an allowed external URL that 302-redirects to an internal one — many fetchers follow redirects after validating only the first URL.
- DNS rebinding: a hostname that resolves to a public IP at validation time and an internal IP at fetch time (time-of-check vs time-of-use).
- Parser differentials: URLs like
http://allowed.com@169.254.169.254/where the validator and the HTTP client disagree about the host. - Blind SSRF: when nothing is reflected, an out-of-band interaction server (Burp Collaborator / interactsh) confirms the request happened.
How I fix it as a developer
No single control is enough; I layer them:
- Prefer allowlists over blocklists. If the feature only needs to fetch from three partner domains, allow exactly those.
- Resolve, then validate, then connect to the validated IP. Resolve the hostname yourself, reject private, loopback and link-local ranges, and pin the connection to that IP so rebinding can't swap it. Re-validate on every redirect, or disable redirects.
- Isolate the fetcher. Run URL-fetching workers in a separate network segment or egress proxy with no route to metadata or internal services.
- Enforce IMDSv2 everywhere and keep the hop limit at 1 unless containers truly need 2.
- Least-privilege IAM. If the role can only read one bucket prefix, a stolen credential can only read one bucket prefix.
- Detect. Alert on instance credentials being used from outside your own IP ranges — GuardDuty has findings for exactly this.
The takeaway
SSRF is a developer bug with an infrastructure blast radius, which is why it needs both mindsets to fix properly: secure URL handling in code, and a cloud configuration that assumes the code will eventually fail.
References
- AWS Docs — Use IMDSv2
- AWS Security Blog — Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service
- OWASP — Server-Side Request Forgery Prevention Cheat Sheet
- PortSwigger Web Security Academy — Server-side request forgery