Symptoms
Right after a security change that set HttpTokens=required on instances or launch templates:
- The AWS CLI works on the host, but inside a container it fails with
Unable to locate credentials/NoCredentialProviders/Could not connect to the endpoint URL: "http://169.254.169.254/...". - SDK calls hang for a few seconds before failing (the SDK retries IMDS, then gives up).
- Pods on EKS nodes that relied on the node role lose access to S3 or ECR.
Why it happens
IMDSv2 starts with a PUT /latest/api/token request. The response comes back with an IP TTL equal to the hop limit (default 1). A container on a bridge network is one extra network hop away from the host. With a hop limit of 1, the token response is dropped before it reaches the container, so IMDSv2 fails. With IMDSv1 disabled, there's no fallback.
| Where the caller runs | Hops to IMDS | Works with hop limit 1? |
|---|---|---|
| Host process | 1 | Yes |
Docker --network host | 1 | Yes |
Docker bridge / ECS bridge mode | 2 | No |
| Kubernetes pod (most CNIs, non-host network) | 2 | No |
Step 1: Confirm the metadata options
aws ec2 describe-instances --instance-ids i-0123456789abcdef0 \
--query 'Reservations[].Instances[].MetadataOptions'
Look for "HttpTokens": "required" and "HttpPutResponseHopLimit": 1.
Reproduce from inside the container:
TOKEN=$(curl -s -m 3 -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
echo "token length: ${#TOKEN}"
curl -s -m 3 -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Empty token from the container but a real token from the host = hop limit.
Step 2a: Raise the hop limit (quick fix)
aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 \
--http-tokens required --http-put-response-hop-limit 2 --http-endpoint enabled
Make it permanent in the launch template so new instances from an Auto Scaling group get it:
aws ec2 create-launch-template-version --launch-template-id lt-0abc \
--source-version '$Latest' \
--launch-template-data '{"MetadataOptions":{"HttpTokens":"required","HttpPutResponseHopLimit":2,"HttpEndpoint":"enabled"}}'
aws ec2 modify-launch-template --launch-template-id lt-0abc --default-version '$Latest'
Then roll the ASG (instance refresh) so running instances pick it up, or modify them directly as above.
Warning
Hop limit 2 lets every container on the host use the instance role. On shared nodes that is often more access than you want. Treat it as a stop-gap and move to per-workload roles (Step 2b).
Step 2b: Give workloads their own role (proper fix)
| Platform | Use | The SDK reads credentials from |
|---|---|---|
| ECS on EC2 | Task role (taskRoleArn) | ECS agent endpoint 169.254.170.2 via AWS_CONTAINER_CREDENTIALS_RELATIVE_URI |
| EKS | EKS Pod Identity or IRSA | Pod Identity agent / web identity token file |
| Plain Docker | Pass credentials from a sidecar or use host networking for one trusted container | — |
With task roles or Pod Identity, containers never need IMDS, and you can keep hop limit 1 to block containers from the node role. For ECS tasks in awsvpc mode, you can also set ECS_AWSVPC_BLOCK_IMDS=true in /etc/ecs/ecs.config to block tasks from IMDS entirely; for bridge mode, the ECS documentation describes an iptables rule that does the same. Check the ECS agent configuration docs for your agent version.
Check what credentials a container is actually using:
aws sts get-caller-identity
env | grep -E 'AWS_CONTAINER_CREDENTIALS|AWS_WEB_IDENTITY|AWS_ROLE_ARN'
Step 3: Other causes that look the same
| Cause | How to spot it | Fix |
|---|---|---|
HttpEndpoint=disabled | MetadataOptions shows it | Enable, or move to task roles |
| Proxy env vars in the container | HTTP_PROXY set, no NO_PROXY for 169.254.169.254 | Add 169.254.169.254,169.254.170.2 to NO_PROXY |
| iptables rule blocking metadata | iptables -t nat -L -n shows a DROP/REDIRECT for 169.254.169.254 | Intended by your platform? Then use task/pod roles |
| Kubernetes NetworkPolicy blocks egress to 169.254.169.254 | Policy egress rules | Allow it, or use Pod Identity |
| Old SDK that only speaks IMDSv1 | SDK version older than IMDSv2 support | Upgrade the SDK/CLI |
| No role attached to the instance | iam/security-credentials/ is empty from the host too | Attach an instance profile |
Check SDK minimum versions in the AWS IMDSv2 documentation before blaming the hop limit on very old images.
Step 4: Find who still needs IMDSv1 before you enforce
Before switching more fleets to required, look at the MetadataNoToken CloudWatch metric per instance. Non-zero means something still makes IMDSv1 calls:
aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name MetadataNoToken \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time $(date -u -d '-1 day' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
--period 3600 --statistics Sum
Prevent it next time
- Roll out IMDSv2 in this order: watch
MetadataNoToken, upgrade SDKs, move containers to task/pod roles, then enforcerequired. - Set the hop limit explicitly in every launch template (1 for VMs only, 2 only where containers truly need the node role).
- Use an account-level default (
aws ec2 modify-instance-metadata-defaults) so new instances launch with IMDSv2 and the hop limit you chose. - Add a container start-up check that calls
sts get-caller-identityand fails fast with a clear message.