HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 1142 min read

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?