Skip to main content
emnode / learn
Compliance

Enforce IMDSv2 on EC2

One capability across running instances and the launch sources that create them: require the IMDSv2 session-token handshake everywhere so a server-side request forgery bug cannot read an instance's role credentials.

13 min·10 sections·AWS

Last reviewed

Remediates AWS Security Hub: AutoScaling.3EC2.8

Enforcing IMDSv2: the basics

What is the Instance Metadata Service and why does the version matter?

Every EC2 instance can reach a link-local endpoint at 169.254.169.254, the Instance Metadata Service (IMDS). It returns the instance's identity, user-data, and most importantly the temporary IAM credentials of the attached instance profile. Anything running on the instance with HTTP egress can ask for those credentials and immediately have whatever the role grants. IMDSv1 answers any unauthenticated GET; IMDSv2 first requires a session token obtained via a PUT with a custom header, and almost no server-side request forgery (SSRF) primitive can issue an arbitrary PUT with a header, so the credentials become unreachable from that whole class of bug.

AWS Security Hub turns this into more than one control because enforcement has to hold at two layers. EC2.8 fails any running instance whose MetadataOptions.HttpTokens is optional rather than required. AutoScaling.3 fails any Auto Scaling launch configuration that still permits IMDSv1, because the fleet that LC governs keeps launching IMDSv1-permissive instances regardless of what you fixed on the running boxes. They look like separate findings, but they are one capability: require the IMDSv2 handshake on the instances and on the sources that create them.

The trap is that fixing one layer alone leaves the other open. Flip a running instance to required and AutoScaling.3 still fails if the launch source is permissive; migrate the launch source and EC2.8 still fails until the existing instances are replaced. The job is to inventory everything still on IMDSv1, flip the running fleet, fix the launch sources so new instances are born compliant, and roll the old ones out, then lock the setting at the account level.

In this lesson you will learn why IMDSv1 has been the root cause of some of the largest cloud breaches on record, how IMDSv2 closes the SSRF window at the HTTP layer, how to check whether your fleet and its launch sources are compliant, and how to migrate safely, the SDK compatibility check, the hop-limit decision, the instance refresh, and the launch-source change that makes new instances born compliant. The Controls this lesson covers section lists every Security Hub control in this capability, each linking to a deep page with the exact check and a copy-and-paste fix.

Fun fact

Capital One: 100 million records, one IMDSv1 call

In 2019 an attacker exploited a server-side request forgery in a misconfigured Capital One WAF to make the WAF itself fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/. IMDSv1 returned the role's temporary credentials, the attacker assumed them externally, and used them to list and download S3 buckets, exfiltrating roughly 106 million customer records. AWS announced IMDSv2 later that same year as the direct architectural fix. A single PUT-required token would have stopped the attack at step one, because a forged GET cannot obtain the session token.

Finding IMDSv1 across an estate

Marco is the security lead at a fintech. A Security Hub scan fires EC2.8 across 47 production instances and AutoScaling.3 across three launch configurations, all still permitting IMDSv1. The fleet mixes EKS workers, legacy batch jobs and a few Windows servers nobody really owns.

He cannot just flip everything to required, since a workload using an SDK old enough to fall back to IMDSv1 only would start failing the moment the flag flips. So he starts by listing the offending instances and their hop limits, to see the spread before changing anything.

List every running instance whose metadata options still allow IMDSv1. HttpTokens=optional is the EC2.8 failure signal.

$ aws ec2 describe-instances --query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].{Id:InstanceId,Hop:MetadataOptions.HttpPutResponseHopLimit,State:State.Name}" --output table
--------------------------------------------------
| i-0abc12def345f6789 | 2 | running |
| i-0cde34f0567b8901c | 1 | running |
--------------------------------------------------
# 47 total. EKS workers at hop=2 (intentional for pods), legacy boxes at hop=1.

Every instance with HttpTokens=optional is exposing IMDSv1 to anything running locally. The launch sources behind them need fixing too.

How IMDSv2 closes the SSRF window, at both layersdeep dive

IMDSv2 is session-oriented over the same 169.254.169.254 endpoint. A client first issues a PUT to /latest/api/token with the header X-aws-ec2-metadata-token-ttl-seconds, and the service returns an opaque session token valid for up to six hours. Every subsequent GET must carry that token. The reason this defeats SSRF specifically is that the vast majority of SSRF primitives are GET-only or cannot set arbitrary headers, so the credential read is no longer a single one-shot GET they can replicate.

EC2.8 reads MetadataOptions.HttpTokens on the running instance and fails on optional. AutoScaling.3 reads the same field on the Auto Scaling launch configuration. Launch configurations are the weak point: AWS added the MetadataOptions field to them late in their life, so older ones simply do not set it and the instances inherit the AMI default, which for most public AMIs is still optional. Launch configurations have been deprecated since 2023, which is why AutoScaling.3 commonly fails alongside the control that tells you to migrate to launch templates, and why the right fix is migration, not just patching the LC.

The HttpPutResponseHopLimit setting controls how many network hops the token response can traverse. The default of 1 means only processes on the host network namespace can reach IMDS, so containers with their own namespace (the default for Kubernetes pods) cannot reach it at all. If pods legitimately need instance-role metadata you either use IRSA (the right answer) or raise the hop limit to 2. Crucially, swapping a launch source does not reboot existing instances; they keep running IMDSv1-permissive until replaced by a scaling event, a deploy, or a deliberate instance refresh, so without the refresh AutoScaling.3 clears but EC2.8 keeps failing.

What is the impact of leaving IMDSv1 enabled?

The direct impact is credential exposure. Every SSRF, every XXE that resolves URIs, every container with broken network isolation, every third-party agent with a parser bug becomes a credential-theft primitive on an IMDSv1-permissive instance. The instance's role credentials are the keys to whatever that role can do: read S3, decrypt KMS keys, list Secrets Manager entries, assume other roles. The blast radius is the IAM policy, not the instance, and the credentials are valid long enough to enumerate and exfiltrate.

The second-order impact is that this is a quiet attack. There is no new compute, no new access key, nothing in the cost report to notice; an attacker who finds an SSRF on a permissive instance inherits a ready-made credential-theft step rather than having to build one. The historical record is well documented: the Capital One breach, plus a steady stream of crypto-mining campaigns and incident-response reports where IMDSv1 was the lateral-movement step after an initial app compromise.

On the compliance side, these controls map to the AWS Foundational Security Best Practices standard, PCI DSS, the CIS AWS Foundations Benchmark and NIST 800-53. A HIGH EC2.8 finding on production instances, or an AutoScaling.3 failure on a fleet still launching permissive instances, is audit evidence that least-privilege controls on instance identities were not enforced, and it can hold up a SOC 2 Type II opinion or trigger a PCI compensating-control writeup.

How do you enforce IMDSv2 safely?

Work the capability as one loop rather than chasing individual findings. The order matters: verify compatibility before you flip, fix the launch sources, roll the fleet so the running instances actually pick up the change, and lock it at the account level.

1. Inventory every instance and launch source still on IMDSv1

Use describe-instances filtered on HttpTokens==optional for EC2.8, and describe-launch-configurations filtered on HttpTokens != required for AutoScaling.3. Group by owner, application and instance profile, and rank by what the attached IAM role can reach. The migration order is usually newest, simplest, highest-privilege workloads first; legacy unowned boxes last with a deprecation plan.

2. Verify SDK compatibility before flipping the switch

Modern SDKs (Boto3, AWS CLI v2, the Java, Go, .NET and JavaScript v2/v3 SDKs) all speak IMDSv2 natively. Older versions and any code that hand-rolls metadata calls without the PUT step will break the moment HttpTokens flips to required. Check the SDK versions in your container images; rebuild any pinned image old enough to predate native IMDSv2 support before migration. Skipping this step is the single most common cause of a self-inflicted outage.

3. Flip running instances, fix launch sources, then roll the fleet

Run modify-instance-metadata-options --http-tokens required on running instances, choosing the hop limit deliberately (1 by default; 2 only where non-IRSA container access to IMDS is genuinely needed). For Auto Scaling, migrate the deprecated launch configuration to a launch template with HttpTokens=required, repoint the ASG, and run start-instance-refresh so existing instances are replaced, otherwise AutoScaling.3 clears but EC2.8 keeps failing on the old boxes.

4. Lock it at the account level so new instances are born compliant

Set the account-wide default with modify-instance-metadata-defaults --http-tokens required so any new instance defaults to IMDSv2 regardless of launch source, and update every launch template and node group. Add an SCP denying ec2:RunInstances without IMDSv2 and one denying autoscaling:CreateLaunchConfiguration outright, and pair with the relevant AWS Config rules for continuous detection.

# Flip a running instance to IMDSv2-only (EC2.8).
aws ec2 modify-instance-metadata-options --instance-id i-0abc12def345f6789 \
  --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled

# Migrate the launch source to a launch template with IMDSv2 required (AutoScaling.3),
# then roll the fleet so existing instances actually pick it up.
aws ec2 create-launch-template-version --launch-template-id lt-0fee123abc456def0 \
  --source-version '$Latest' \
  --launch-template-data '{"MetadataOptions":{"HttpTokens":"required","HttpPutResponseHopLimit":2,"HttpEndpoint":"enabled"}}'
aws autoscaling start-instance-refresh --auto-scaling-group-name etl-workers-asg

# Lock it account-wide so new instances are born compliant.
aws ec2 modify-instance-metadata-defaults --http-tokens required --http-put-response-hop-limit 2

Quick quiz

Question 1 of 5

Security Hub shows EC2.8 on running instances and AutoScaling.3 on a launch configuration. What is the most efficient way to think about them?

You can now treat IMDSv2 enforcement as one capability rather than two findings: inventory every instance and launch source still on IMDSv1, verify SDK compatibility, flip the running fleet, migrate the launch sources and instance-refresh so the change actually applies, then lock the setting at the account level so new instances are born compliant. The Controls this lesson covers section below links every control in this group to its deep page and fix.

Back to the library

Controls this lesson covers

One capability, many AWS Security Hub controls. This lesson is the shared playbook; each control below keeps its own deep page with the exact check, severity and a copy-and-paste fix.