IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

TL;DR

Stop embedding IAM User credentials in your EC2 instances. Use an IAM Role attached to the instance profile instead. It is more secure, requires zero credential management, and is the AWS-recommended standard.

AttributeIAM UserIAM Role
Identity TypePermanent identity for a person or serviceTemporary identity assumed by a trusted entity
CredentialsLong-lived Access Key ID + Secret (static)Short-lived STS tokens (auto-rotated)
Rotation RequiredYes — manual or scriptedNo — AWS rotates automatically
Best ForHuman operators, CI/CD pipelines (OIDC preferred)EC2, Lambda, ECS, cross-account access
Risk if LeakedHigh — static key valid until manually revokedLow — token expires in 1–12 hours
EC2 Recommendation❌ Never✅ Always

The Core Problem: Static Credentials Are a Liability

An IAM User has a permanent Access Key ID and Secret Access Key. When you embed these in an EC2 instance — via ~/.aws/credentials, an environment variable, or a config file — you create a static secret that lives on disk. If the instance is compromised, the attacker gets credentials that remain valid until you manually revoke them. This is the root cause of the majority of AWS credential leak incidents reported on platforms like GitHub.

The Analogy: An IAM User credential on an EC2 instance is like giving a contractor a master key to your building and letting them keep it forever. An IAM Role is like issuing a keycard that expires at the end of the day — even if they copy it, it's useless tomorrow.

How IAM Roles Work on EC2: The Data Flow

When you attach an IAM Role to an EC2 instance via an Instance Profile, the AWS SDK and CLI automatically retrieve temporary credentials from the Instance Metadata Service (IMDS). You write zero credential code.

Your Application Code
  -> AWS SDK (boto3, AWS SDK for Java, etc.)
    -> Checks credential chain (no hardcoded keys found)
      -> Queries IMDS endpoint: http://169.254.169.254/latest/meta-data/iam/security-credentials/{role-name}
        -> AWS STS returns: { AccessKeyId, SecretAccessKey, SessionToken, Expiration }
          -> SDK caches token, auto-refreshes before Expiration
            -> API call to S3 succeeds with temporary credentials

The IMDS endpoint 169.254.169.254 is a link-local address only reachable from within the EC2 instance itself. No external exposure. AWS rotates the STS token automatically, typically every 6 hours.

Step-by-Step: Attach an IAM Role to EC2 for S3 Access

Step 1: Create the IAM Policy (Least Privilege)

Grant only the minimum permissions required. If the application reads objects from a specific bucket, scope it precisely.

# policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadOnTargetBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    }
  ]
}
aws iam create-policy \
  --policy-name EC2S3ReadPolicy \
  --policy-document file://policy.json

Step 2: Create the IAM Role with EC2 Trust Policy

# trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
aws iam create-role \
  --role-name EC2S3AccessRole \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy \
  --role-name EC2S3AccessRole \
  --policy-arn arn:aws:iam::123456789012:policy/EC2S3ReadPolicy

Step 3: Create an Instance Profile and Attach the Role

aws iam create-instance-profile \
  --instance-profile-name EC2S3InstanceProfile

aws iam add-role-to-instance-profile \
  --instance-profile-name EC2S3InstanceProfile \
  --role-name EC2S3AccessRole

Step 4: Attach the Instance Profile to Your EC2 Instance

# At launch time
aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --iam-instance-profile Name=EC2S3InstanceProfile \
  --key-name my-key-pair

# OR attach to a running instance
aws ec2 associate-iam-instance-profile \
  --instance-id i-0123456789abcdef0 \
  --iam-instance-profile Name=EC2S3InstanceProfile

Step 5: Enforce IMDSv2 (Security Hardening)

IMDSv2 requires a session-oriented request, blocking SSRF-based credential theft attacks. Always enforce it.

aws ec2 modify-instance-metadata-options \
  --instance-id i-0123456789abcdef0 \
  --http-tokens required \
  --http-endpoint enabled

Terraform Equivalent

resource "aws_iam_role" "ec2_s3_role" {
  name = "EC2S3AccessRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "s3_read" {
  name = "S3ReadPolicy"
  role = aws_iam_role.ec2_s3_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:ListBucket"]
      Resource = [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    }]
  })
}

resource "aws_iam_instance_profile" "ec2_profile" {
  name = "EC2S3InstanceProfile"
  role = aws_iam_role.ec2_s3_role.name
}

resource "aws_instance" "app" {
  ami                  = "ami-0abcdef1234567890"
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.ec2_profile.name

  metadata_options {
    http_tokens   = "required"
    http_endpoint = "enabled"
  }
}

Cost Impact

  • IAM Roles, Policies, and Instance Profiles: Free. AWS does not charge for IAM entities.
  • STS API calls (AssumeRole, GetSessionToken): Free for calls made by AWS services on your behalf (e.g., IMDS-triggered token refresh).
  • S3 API calls (GetObject, ListBucket): Billed per request and per GB transferred. Scoping permissions to a specific bucket does not affect cost, but it prevents accidental access to other buckets that could incur unintended charges.
  • Net cost of switching from IAM User to IAM Role: Zero. It is a pure security improvement with no added cost.

When Is an IAM User Actually Appropriate?

  • Human operators who need AWS Console access with MFA enforced.
  • Legacy CI/CD systems that cannot use OIDC federation (though OIDC is strongly preferred for GitHub Actions, GitLab CI, etc.).
  • Third-party tools that do not support role assumption and require static credentials stored in a secrets manager (e.g., AWS Secrets Manager or HashiCorp Vault) — never in plaintext.

For any workload running inside AWS (EC2, Lambda, ECS, EKS), an IAM Role is always the correct choice.

Glossary

TermDefinition
IAM RoleAn AWS identity with a trust policy that defines which entities can assume it, issuing temporary STS credentials upon assumption.
Instance ProfileA container that holds exactly one IAM Role and is the mechanism by which an EC2 instance assumes that role.
IMDS (Instance Metadata Service)A link-local HTTP endpoint (169.254.169.254) accessible only from within an EC2 instance, used to retrieve instance metadata including temporary IAM credentials.
STS (Security Token Service)The AWS service that issues short-lived, temporary security credentials for assumed roles or federated identities.
Least PrivilegeThe security principle of granting only the exact permissions required for a task and nothing more, minimizing the blast radius of a credential compromise.

Comments

Popular posts from this blog

EC2 No Internet Access in a Custom VPC: Attaching an IGW and Fixing Route Tables

Breaking the Loop: How to Prevent Recursive Lambda Triggers on S3

EC2 SSH 'Connection Timed Out': The Definitive Security Group Diagnosis Guide