Every EC2 instance answers HTTP on 169.254.169.254, a link-local address that never leaves the box. Ask it nicely and it hands back the instance's IAM role credentials — a live AccessKeyId, SecretAccessKey, and session token:

bash
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/example-role

Under IMDSv1 that is the entire protocol: one GET, no authentication, no headers. Which is fine when the caller is your own code, and catastrophic when the caller is your web app being puppeteered by someone else. Any server-side request forgery (SSRF) bug — a URL-preview feature, a webhook tester, a misconfigured reverse proxy that forwards arbitrary paths — becomes a credential dispenser, because from the metadata service's point of view, a forged request from your app is your app.

IMDSv2, which AWS shipped in November 2019, closes this without changing what the service knows about its callers — which is almost nothing. It can't authenticate you; anything running on the instance is supposed to be able to use it. What it can do is exploit three asymmetries between a legitimate local caller and a confused deputy. Each one is a boring protocol detail. Together they're remarkably hard to get around.

Defense 1: the session opens with a PUT

IMDSv2 is session-oriented. Before reading any metadata, you request a token, and the token request must be an HTTP PUT:

bash
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/

The token lives between 1 second and 6 hours (21,600 seconds) — you choose via the TTL header — and every subsequent GET must carry it. When the instance is configured to require tokens, a request without a valid one gets 401 Unauthorized.

Why PUT, specifically? Because almost nothing that forwards attacker-controlled requests will emit one. AWS's design rationale is explicit on this: among third-party WAF and proxy products they surveyed, "the vast majority do not permit HTTP PUT requests." The same holds for typical SSRF primitives — a URL-fetch feature in an application overwhelmingly issues GETs, and very few SSRF bugs let the attacker choose the method and set arbitrary request headers and read the response. The token requirement converts "can make the server fetch a URL" from a full compromise into a dead end.

This is defense by statistical rarity, not by cryptographic impossibility, and AWS doesn't pretend otherwise. An SSRF gadget that gives full control over method, headers, and body can still walk through the handshake. But that's a dramatically smaller class of bug than "can influence a URL."

Defense 2: X-Forwarded-For is a confession

The second check targets open reverse proxies. A proxy that forwards external requests to internal addresses is a classic path to the metadata service — and it will happily send a PUT if the client asks for one. But well-behaved proxies do something else out of habit: they append an X-Forwarded-For header identifying the original client.

IMDSv2 treats that header as disqualifying. Token-issuing PUT requests that contain X-Forwarded-For are rejected outright. A legitimate process on the instance calling the service directly would never add it; its presence is near-proof that the request took at least one HTTP hop through software that knew it was relaying for someone else. The proxy's own honesty becomes the tripwire.

Defense 3: the response packet cannot survive a hop

The third defense doesn't live in HTTP at all. It lives in the IP header, in a field standardized in 1981: time-to-live. By default, the metadata service sends its token response in packets with TTL = 1.

Every router and every packet-forwarding hop decrements TTL by one and discards packets that hit zero. So a token response with TTL 1 can be delivered to a process on the instance itself — zero hops — and nowhere else. If a NAT box, a container router, or a compromised neighboring machine tries to relay the metadata service's answer onward, the network layer silently eats it. The request may arrive; the token never leaves.

This is the backstop for everything the first two checks miss. Even a perfect SSRF gadget running on some other machine that routes packets through your instance gets nothing back, because the response dies in transit. The token is also instance-bound at the service level: AWS documents that a token "is not valid on other EC2 instances and will be rejected if you attempt to use it outside of the instance on which it was generated."

There's one deliberate escape hatch. Containers often sit behind a virtual bridge, which counts as a hop — so a pod's token response would die before reaching it. The hop limit is therefore configurable via modify-instance-metadata-options, and Amazon Linux 2023 AMIs default to a hop limit of 2 precisely so containers can reach the IMDS. That's also the knob to scrutinize in a review: every increment widens the blast radius the TTL trick was buying you.

What IMDSv2 does not fix

It's worth being precise about the threat model, because IMDSv2 is often oversold:

  • Code execution on the box still wins. Anything that can run curl locally can do the PUT handshake. IMDSv2 defends against request forgery, not against a compromised process. For stolen-credential use, the complementary controls are IAM condition keys like aws:EC2InstanceSourceVPC and aws:EC2InstanceSourcePrivateIPv4, which pin an instance role's credentials to requests originating from the instance's own network path.
  • Fully controllable SSRF still wins. Method + header + response control defeats defense 1, and such gadgets do exist — they're just rare.
  • It's opt-in-by-configuration. IMDSv1 and v2 run side by side by default on older setups; the protections only bite when HttpTokens=required turns v1 off. Datadog Security Labs measured that as of September 2022, roughly 93% of EC2 instances did not enforce IMDSv2 — three years after it shipped. The mechanism was never the hard part; the migration was, because every SDK, agent, and init script touching the metadata endpoint had to speak the token handshake first (AWS publishes minimum SDK versions — e.g., botocore 1.13.25, AWS SDK for Go 1.25.38).

Enforcing it

Per instance, enforcement is one flag: launch (or modify) with HttpTokens=required, which makes tokenless requests fail with 401. Fleet-wide, a service control policy can refuse to launch anything less:

json
{
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Condition": {
    "StringNotEquals": { "ec2:MetadataHttpTokens": "required" }
  }
}

The deeper lesson generalizes beyond AWS. The metadata service can't cryptographically authenticate "the real workload," so IMDSv2 instead stacks cheap asymmetries: a verb proxies won't forward, a header proxies can't help adding, and a packet that physically cannot travel. None is sufficient; each fails differently; an attacker must thread all three at once. That's what defense in depth looks like when it's designed rather than accumulated — and it still only works if you turn it on.

References