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.
| Attribute | IAM User | IAM Role |
|---|---|---|
| Identity Type | Permanent identity for a person or service | Temporary identity assumed by a trusted entity |
| Credentials | Long-lived Access Key ID + Secret (static) | Short-lived STS tokens (auto-rotated) |
| Rotation Required | Yes — manual or scripted | No — AWS rotates automatically |
| Best For | Human operators, CI/CD pipelines (OIDC preferred) | EC2, Lambda, ECS, cross-account access |
| Risk if Leaked | High — static key valid until manually revoked | Low — 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
| Term | Definition |
|---|---|
| IAM Role | An AWS identity with a trust policy that defines which entities can assume it, issuing temporary STS credentials upon assumption. |
| Instance Profile | A 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 Privilege | The security principle of granting only the exact permissions required for a task and nothing more, minimizing the blast radius of a credential compromise. |
Comments
Post a Comment