EC2 instances should use IMDSv2
The EC2 Instance Metadata Service exposes IAM role credentials at http://169.254.169.254. IMDSv1 serves these credentials to any process that sends a simple HTTP GET, which means a server-side request forgery (SSRF) vulnerability in any application on the instance can exfiltrate temporary IAM credentials in a single hop. The Capital One breach in 2019 exploited exactly this pattern.
IMDSv2 adds a PUT-based session token handshake with a TTL-limited token and a default hop limit of 1, which blocks most SSRF exploitation paths because the attacker cannot inject the required custom header through typical SSRF vectors. Enforcing IMDSv2 across all instances is one of the highest-value, lowest-effort security improvements available for EC2 workloads.
Retrofit consideration
Applications or scripts that call the metadata endpoint without IMDSv2 token negotiation (older AWS SDK versions, custom curl calls to http://169.254.169.254) will break when IMDSv1 is disabled. Audit running instances with aws ec2 describe-instances and check the CloudWatch metric MetadataNoToken before enforcing.
Implementation
Choose the approach that matches how you manage Terraform.
Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.
module "ec2_instance" {
source = "pcidss.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "hipaa.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nist80053.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "fedrampmoderate.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cis.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cisv80ig1.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nydfs23.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cisv500.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "cfrpart11.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "fedramplow.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nistcsfv11.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
module "ec2_instance" {
source = "nist80053rev4.compliance.tf/terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
}
If you use terraform-aws-modules/ec2-instance/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "ec2_instance" {
source = "terraform-aws-modules/ec2-instance/aws"
version = ">=6.0.0"
ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
instance_type = "t4g.nano"
subnet_id = "subnet-abc123"
metadata_options = {
http_tokens = "required"
}
}
Use AWS provider resources directly. See docs for the resources involved: aws_instance.
resource "aws_instance" "this" {
ami = "ami-abc12345"
instance_type = "t4g.nano"
subnet_id = element(["subnet-abc123", "subnet-def456"], 0)
vpc_security_group_ids = ["sg-abc12345"]
metadata_options {
http_tokens = "required"
}
}
What this control checks
The control checks that every aws_instance and aws_launch_template resource includes a metadata_options block with http_tokens set to "required". With http_tokens = "required", the instance only responds to metadata requests that carry a valid session token obtained via PUT request. Omitting metadata_options entirely, or setting http_tokens to "optional", fails the control since IMDSv1 GET requests remain accepted in either case. The http_endpoint argument must also stay "enabled" (the default) for metadata to function at all. http_put_response_hop_limit defaults to 1, preventing token forwarding through extra network hops; container workloads may need this set to 2. For Auto Scaling Groups, the enforcement point is the aws_launch_template, not the ASG resource itself.
Common pitfalls
Omitting metadata_options defaults to IMDSv1 allowed
Declaring
aws_instancewithout ametadata_optionsblock leaveshttp_tokensat"optional", the AWS API default. Terraform shows no drift because the provider treats the omission as accepting that default. You must explicitly sethttp_tokens = "required"in every instance and launch template resource.Launch templates can be overridden at launch time
Instance-level
metadata_optionsblocks override the launch template at launch time. If anaws_instanceor Auto Scaling Group referencing the template supplies its ownmetadata_options, that wins. Check the effective configuration on the instance, not just the template definition.Older SDKs and user-data scripts will break on enforcement
Enforce IMDSv2 before auditing your SDK versions and user-data scripts and things will break in ways that aren't always obvious. AWS SDK versions before mid-2019 (boto3 < 1.13.0, Java SDK < 1.11.x with IMDS support) don't negotiate IMDSv2 tokens;
curl http://169.254.169.254/latest/meta-data/calls without the PUT token exchange return HTTP 401. Check the CloudWatchMetadataNoTokenmetric on each instance before flipping enforcement.Container workloads may need hop limit of 2
Set
http_put_response_hop_limit = 2for any instance running Docker, ECS on EC2, or other container workloads. Docker adds a network hop between the container and the metadata endpoint; with the default hop limit of1, containers can't reach IMDS at all.
Audit evidence
Auditors expect the AWS Config rule ec2-imdsv2-check showing all evaluated resources as COMPLIANT, or equivalent output from a CSPM tool. The CLI query aws ec2 describe-instances --query 'Reservations[*].Instances[*].{Id:InstanceId,HttpTokens:MetadataOptions.HttpTokens}' should return "required" for every running instance. For accounts using account-level IMDS defaults, aws ec2 get-instance-metadata-defaults should show HttpTokens: required.
CloudTrail records for RunInstances and ModifyInstanceMetadataOptions document when IMDSv2 enforcement was applied and whether it was ever reverted. If the organization enforces IMDSv2 via SCP or IAM condition key (ec2:MetadataHttpTokens), the auditor will typically also request the SCP policy document as a preventive control artifact.
Framework-specific interpretation
PCI DSS v4.0: PCI DSS v4.0 Requirement 6.3 covers protection against known attack techniques, and SSRF-based metadata credential theft is one of the documented ones. Enforcing IMDSv2 is one way to satisfy the secure configuration intent here, though it maps more cleanly to Requirement 7 (restrict access to system components) than to the software inventory side of Requirement 6.
HIPAA Omnibus Rule 2013: Any EC2 workload with access to ePHI is a problem if SSRF can drain the instance's IAM credentials. The HIPAA Security Rule's technical safeguards under 45 CFR 164.312(a)(1) require limiting credential access to authorized processes, and IMDSv2 is one technical mechanism that tightens that boundary. It doesn't satisfy the access control requirement on its own, but it closes a well-documented gap.
NIST SP 800-53 Rev 5: AC-3 (Access Enforcement) and SI-10 (Information Input Validation) both apply. AC-3 because IMDS exposes IAM credentials that must be access-controlled; SI-10 because IMDSv2 validates that metadata requests carry a legitimate session token rather than accepting arbitrary unauthenticated GETs. SC-7 is also relevant where an SSRF path traverses a network boundary to reach the metadata endpoint.
FedRAMP Moderate Baseline Rev 4: At the Moderate baseline, AC-3 and SC-7 both apply here. Federal workloads on EC2 must prevent lateral credential exposure, and the SSRF-to-metadata path is well-documented in federal cloud guidance. IMDSv2 enforcement limits the blast radius of any SSRF vulnerability on a government system and is what agency reviewers ask to see.
Related controls
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
ec2_instance_uses_imdsv2AWS Config Managed Rule:
EC2_IMDSV2_CHECKCheckov Check:
CKV_AWS_79Powerpipe Control:
aws_compliance.control.ec2_instance_uses_imdsv2Prowler Checks:
ec2_instance_account_imdsv2_enabled,ec2_instance_imdsv2_enabledAWS Security Hub Control:
EC2.8KICS Query:
c306ac53-ee5b-41d3-86a9-0fd2722b4e67Trivy Checks:
AWS-0028,AWS-0130
Last reviewed: 2026-03-09