Instance Metadata Service
The instance metadata service is the link-local endpoint a cloud instance queries for its own metadata and role credentials. How IMDSv1 and IMDSv2 differ.
The instance metadata service (IMDS) is a local HTTP endpoint that a cloud virtual machine queries to read facts about itself: its instance id, its instance type, the region and availability zone it runs in, its network addresses, and the temporary credentials of any identity role attached to it. It answers on a link-local address that is reachable only from inside that one instance, which is why no hostname, no credential, and no network route is needed to call it.
Every major cloud exposes one, and every cloud SDK reads it. A build agent that never mentions an access key still ends up authenticated because the SDK walked its default credential chain down to the metadata endpoint and found a role there.
Definition
The metadata service listens on the link-local IPv4 address 169.254.169.254 on port 80, with an IPv6 form of fd00:ec2::254 on AWS. Link-local means the address is never routed: a packet sent to it is answered by the hypervisor on the host running that instance, so the same URL returns different data on every machine and returns nothing at all from outside.
Two things live behind that address, and they carry very different risk.
The first is descriptive metadata. Instance id, instance type, placement, AMI or image id, hostname, private and public IPv4 addresses, and any user data script passed at launch. Reading it tells a caller where it is running.
The second is credentials. When a role is attached to the instance, the service issues short-lived credentials for that role on request. On AWS those live under /latest/meta-data/iam/security-credentials/<role-name> and return an access key id, a secret access key, a session token, and an expiry timestamp as JSON. The SDK refreshes them before the expiry without the workload doing anything.
That second capability is what makes the endpoint interesting to an attacker. Any code path on the instance that can be tricked into fetching an attacker-supplied URL, the class of bug called server-side request forgery, can be pointed at 169.254.169.254 and made to return role credentials in the HTTP response body.
The two request protocols
IMDSv1 is a request and response protocol. A plain GET to the metadata path returns the value. Nothing distinguishes a fetch made by the workload from a fetch made by a forged request that the workload was tricked into issuing.
IMDSv2 is session oriented. The caller first sends a PUT to /latest/api/token with a header naming how long the session should last, receives an opaque token in the response body, and then attaches that token as a header on every metadata GET. Three properties follow from that shape (AWS instance metadata service documentation, checked on 2026-08-13):
| Property | IMDSv1 | IMDSv2 |
|---|---|---|
| First request | GET /latest/meta-data/... | PUT /latest/api/token |
| Auth on data requests | None | X-aws-ec2-metadata-token header |
| Session lifetime | Not applicable | 1 to 21600 seconds, set by the caller |
X-Forwarded-For on the token request | Not applicable | Rejected |
| Default response hop limit | Instance default | 1 |
| Reachable by a forged plain GET | Yes | No |
The PUT requirement blocks the common forgery shape, because a URL-fetching bug usually issues a GET and cannot set request headers. Rejecting token requests that carry X-Forwarded-For blocks the reverse-proxy shape, where a misconfigured proxy on the instance relays an outside request inward. The hop limit is an IP time-to-live on the token response: at the default of 1, the response dies at the instance boundary, so a container on a bridge network or a downstream host never receives a usable token.
An instance can be set to accept both protocols or to require IMDSv2, in which case unauthenticated GET requests are refused. Requiring the token version is the setting that closes the forgery path; supporting it while still answering v1 does not.
The same idea on other clouds
The pattern is not unique to one provider. Azure serves its Instance Metadata Service at the same 169.254.169.254 address under /metadata/instance, and rejects any request that omits the Metadata: true header. Google Cloud serves its metadata server at metadata.google.internal and requires a Metadata-Flavor: Google header. Both header requirements exist for the reason IMDSv2 exists: a forged plain GET cannot set them.
Example
A GitHub Actions job running on a self-hosted runner in a cloud account inherits whatever role is attached to that instance. This job reads the region and the attached role name from the metadata service using the token protocol, then lets the AWS CLI resolve credentials through the same endpoint.
name: build
on:
push:
branches: [main]
jobs:
package:
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v4
- name: Read instance metadata
run: |
TOKEN=$(curl -sS -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
curl -sS -H "X-aws-ec2-metadata-token: $TOKEN" \
"http://169.254.169.254/latest/meta-data/placement/region"
curl -sS -H "X-aws-ec2-metadata-token: $TOKEN" \
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
- name: Push the image
run: |
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin "$REGISTRY"
docker build -t "$REGISTRY/app:$GITHUB_SHA" .
docker push "$REGISTRY/app:$GITHUB_SHA"
env:
REGISTRY: ${{ vars.ECR_REGISTRY }}The same job written against IMDSv1 drops the PUT and the header:
- name: Read instance metadata (v1 shape)
run: |
curl -sS "http://169.254.169.254/latest/meta-data/placement/region"On an instance that requires IMDSv2, that second form returns HTTP 401 and the step fails. The aws ecr get-login-password step in the first job keeps working either way, because current AWS SDK and CLI releases request a token first and fall back only when the instance still allows v1.
Two failure modes show up repeatedly in GitHub Actions when the token version is required:
| Symptom | Cause | Fix |
|---|---|---|
Every metadata curl returns 401 | Step issues a plain GET with no token | Issue the PUT first and pass the token header |
Steps work directly but fail inside a container: job | Response hop limit of 1 stops the token at the instance boundary | Raise the hop limit to 2, or pass credentials into the container explicitly |
The second row is worth a workflow-level note. A job that sets jobs.<job_id>.container runs its steps inside a Docker container on the runner, which adds a network hop between the step and the host. Nothing about the workflow file changes; the same commands that succeeded as host steps start timing out or returning 401 inside the container.
Related Terms
- Requiring IMDSv2 on BYOC runner instances: how the token version is required on runners in your own cloud account, and what to check in build steps before turning it on.
- IAM instance profile, defined: the container that attaches a role to an instance, which is what puts credentials behind the metadata endpoint in the first place.
- Does BYOC work with IMDSv2 required: the direct answer for teams running runners in their own cloud account under a policy that mandates the token version.
- AWS BYOC security hardening: the security checklist for runner instances, including least-privilege instance profiles and egress restrictions.
- AWS BYOC runner configuration: where the IMDSv2 requirement is set on a runner.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is the difference between IMDSv1 and IMDSv2?
IMDSv1 answers a plain GET to the link-local address with no authentication step. IMDSv2 is session oriented: the caller first sends a PUT to /latest/api/token with a time-to-live header, receives a session token, and puts that token in a header on every later GET. A request that cannot issue the PUT and read the response headers, which is the shape most server-side request forgery bugs take, cannot reach the metadata.
Why does a container on the instance fail to read instance metadata?
IMDSv2 token responses carry an IP time-to-live, called the response hop limit, that defaults to 1. A packet leaving the instance for a container on a bridge network crosses one hop, so the token response expires before it arrives. Raising the hop limit to 2 lets containers reach the service, and leaving it at 1 keeps metadata inside the instance.
What does the metadata service return that a build might need?
The instance identity fields (instance id, instance type, region, availability zone, private and public IPv4 addresses) and, when a role is attached to the instance, temporary credentials under /latest/meta-data/iam/security-credentials/. Cloud SDKs read that credential path automatically as the last step of their default credential chain.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.