IMDSv1 vs IMDSv2
One HTTP Header Between You and Your Cloud Keys
Cloud infrastructurecloud infrastructure / security
Your EC2 instance has a role. Your code calls S3 with no keys in sight. Where do the credentials come from? A link-local HTTP endpoint at 169.254.169.254 — the Instance Metadata Service. Understanding how it authenticates is the difference between an SSRF bug and an incident report.
Side by side
IMDSv1 — request/response
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
→ 200 OK, temporary AccessKeyId + SecretAccessKey + Token. No auth. No header. Any GET from inside the box wins.
IMDSv2 — session-oriented
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ Needs a PUT first, then a custom header on every read.
Why those two specifics matter
Not because tokens are magic — they're not secret, anyone on the box can mint one. It's that the shape of the request is now hostile to confused deputies:
- PUT — browsers and most SSRF-able URL fetchers only issue GET. An attacker who gets your app to fetch an attacker-supplied URL can't make it PUT.
- Custom request header — blocked by CORS, and not something a "fetch this image URL" feature sets.
- Hop limit (default 1) — the IP TTL on the response. A reverse proxy or a container with its own netns adds a hop, so a forwarded request dies in transit.
The container gotcha
Set hop limit 1 and your pods on bridge/overlay networking lose metadata access entirely — SDK sits there retrying, then "unable to locate credentials." The fix is not hop limit 2 everywhere. It's: hop limit 2 only where the network genuinely adds a hop (EKS nodes), and better, stop using node IMDS from pods at all — use IRSA / Pod Identity so each pod gets its own scoped role.
The real-world failure
2019, Capital One: SSRF in a WAF → GET to 169.254.169.254 → role credentials → ~100M records. IMDSv2 exists because of that class of bug.
Audit it today
Who's still on v1 (CloudWatch, per-instance):
MetadataNoToken — nonzero = something is using v1
Check one instance:
aws ec2 describe-instances --instance-ids i-0abc \
--query 'Reservations[].Instances[].MetadataOptions'
Look for HttpTokens: "optional" ← v1 still allowed.
Enforce:
aws ec2 modify-instance-metadata-options --instance-id i-0abc \
--http-tokens required --http-endpoint enabled --http-put-response-hop-limit 1
New launches: set MetadataOptions in your launch template, and flip the account default so nobody opts out by forgetting.
What people get wrong
- "required is a breaking change for my app" — modern AWS SDKs (botocore ≥1.12.x, JS v3, Go v2) do v2 automatically. The breakage is almost always old SDKs, hand-rolled curl in userdata, or third-party agents.
- Flipping to required without checking MetadataNoToken first. Watch the metric for a week, fix the callers, then enforce.
- Thinking IMDSv2 fixes SSRF. It fixes credential theft via SSRF. Your internal admin service at 10.0.x.x is still wide open.
- Forgetting --http-endpoint. If nothing on the box needs metadata, disable it outright — the strongest setting is "off."
Your exercise
On any instance you own, run the v1 curl. Does it return credentials? Then mint a v2 token and compare. Finally: find one place in your stack where user input becomes a URL your server fetches (webhook target, avatar import, PDF renderer, link preview). What stops that code path from reaching 169.254.169.254?