Skip to content

EC2 instances should use IAM instance roles for AWS resource access

Hardcoded AWS access keys on EC2 instances are a persistent source of credential leaks. They show up in environment variables, configuration files, AMI snapshots, and version control. When an instance uses an IAM instance role, the EC2 metadata service delivers temporary credentials that rotate automatically, removing the need to distribute or manage static secrets.

Instances without an attached role also lack a clear identity boundary. You can't scope AWS permissions to a workload, trace API calls back to a specific instance in CloudTrail, or revoke access by detaching a role. Every instance should have an instance profile, even if its role policy grants zero permissions, because retrofitting one later often requires application restarts and deployment pipeline changes.

Retrofit consideration

Attaching or swapping an instance profile on a running instance doesn't require a stop/start; the EC2 association APIs handle it in place. The harder part is applications that rely on hardcoded credentials: those need code changes to use the SDK default credential chain.

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  = "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  = "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"
}

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"
}

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"]

  iam_instance_profile = "example-instance-profile"
}

What this control checks

The control checks that each aws_instance has iam_instance_profile set to a non-empty value. The instance profile is a separate aws_iam_instance_profile resource that references an aws_iam_role via the role argument. That role must have an assume role policy trusting the ec2.amazonaws.com service principal.

A passing configuration requires:

  1. An aws_iam_role with assume_role_policy allowing sts:AssumeRole from ec2.amazonaws.com.
  2. An aws_iam_instance_profile with its role argument set to that IAM role's name.
  3. An aws_instance with iam_instance_profile set to the instance profile's name or ARN.

It fails when iam_instance_profile is omitted or set to an empty string. The control doesn't evaluate what permissions the attached role grants, only that a profile is present.

Common pitfalls

  • Inline iam_instance_profile name vs. resource reference

    Setting iam_instance_profile to a hardcoded string instead of referencing aws_iam_instance_profile.example.name breaks the Terraform dependency graph. If the profile is deleted or renamed outside Terraform, the instance resource won't detect the drift.

  • Launch templates override instance-level settings

    With aws_launch_template, the IAM instance profile goes inside the iam_instance_profile block using a name or arn argument. If an Auto Scaling group references the template and that block is missing, every launched instance runs without a role, even if a standalone aws_instance in the same module has one configured.

  • Instance profile without attached policies

    An instance profile whose role has no policies satisfies this control but will cause application failures if the workload expects AWS API access. Teams sometimes skip the profile entirely because permissions haven't been decided yet. Attach the profile with a minimal policy and expand it later; leaving it off entirely creates a different problem.

  • Replacing instance profile requires instance restart

    This is a common misconception: changing iam_instance_profile on a running instance doesn't require a stop/start. The aws ec2 associate-iam-instance-profile API attaches a new profile when none exists, and replace-iam-instance-profile-association handles swaps. Terraform uses these APIs automatically. What does require changes is any application code that reads static credentials directly rather than using the SDK default credential chain.

Audit evidence

AWS Config rule results for ec2-instance-profile-attached showing all instances as COMPLIANT are the primary evidence artifact. Prowler or Steampipe scan output works as an equivalent, confirming all instances have a profile attached. Console evidence from the EC2 Instances page with the "IAM Role" column visible, showing no blank entries, satisfies point-in-time checks.

CloudTrail logs for RunInstances events should show iamInstanceProfile populated in the request parameters. AWS Config configuration timeline snapshots for each instance should include a non-null iamInstanceProfile.arn in the recorded configuration item, useful for demonstrating coverage over historical audit periods.

Framework-specific interpretation

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: ec2_instance_using_iam_instance_role

  • AWS Config Managed Rule: EC2_INSTANCE_PROFILE_ATTACHED

  • Checkov Check: CKV2_AWS_41

  • Powerpipe Control: aws_compliance.control.ec2_instance_using_iam_instance_role

  • Prowler Check: ec2_instance_profile_attached

Last reviewed: 2026-03-09