π Hey there, tech enthusiasts!
I'm Sarvar, a Cloud Architect with a passion for transforming complex technological challenges into elegant solutions. With extensive experience spanning Cloud Operations (AWS & Azure), Data Operations, Analytics, DevOps, and Generative AI, I've had the privilege of architecting solutions for global enterprises that drive real business impact. Through this article series, I'm excited to share practical insights, best practices, and hands-on experiences from my journey in the tech world. Whether you're a seasoned professional or just starting out, I aim to break down complex concepts into digestible pieces that you can apply in your projects.
AWS IAM Roles with Terraform: Security Best Practices
"Access keys are like house keys lose them once and you have to change all the locks. IAM roles are like a doorman who knows who you are."
π― Welcome Back!
Remember in Article 8 when you deployed Lambda with API Gateway? You used IAM roles to let Lambda access S3 and CloudWatch. That was just the beginning.
Here's the reality: Every AWS service needs permissions. And there are two ways to grant them:
- Access Keys - Like giving everyone a copy of your house key
- IAM Roles - Like having a smart lock that knows who should enter
Most developers start with access keys because they're simple. Then they face:
- Keys leaked in Git repositories
- Keys stolen from compromised servers
- Keys that never expire
- No idea who's using which keys
- Nightmare rotation process
That's where IAM roles save you.
By the end of this article, you'll:
- β Create IAM roles and policies with Terraform
- β Implement least privilege access
- β Use instance profiles for EC2
- β Attach AWS managed policies
- β Create custom IAM policies
- β Configure S3 bucket policies
- β Never use access keys again
Time Required: 45 minutes (25 min read + 20 min practice)
Cost: $0 (IAM is free, only pay for resources used)
Difficulty: Intermediate
Let's secure your infrastructure properly! π
π The Problem: Access Key Disasters
The Nightmare Scenario
It's 3 AM. Your phone explodes with alerts:
AWS BILLING ALERT
Unusual Activity Detected
Current Charges: $47,000 and climbing
Services: 500 EC2 instances in 12 regions
Cryptocurrency mining detected
Estimated Time: Started 6 hours ago
What happened:
- Developer put AWS access keys in code
- Committed to public GitHub repo
- Bot scraped keys within 15 minutes
- Attacker launched mining operation
- Your bill: $47,000 in one night
This is a real scenario that happens daily.
Common Access Key Mistakes:
β Hardcoded in Application:
# app.py
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCY"
β In Environment Variables (on EC2):
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG"
β In Docker Images:
ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG
β In Configuration Files:
# config.yml
aws:
access_key: AKIAIOSFODNN7EXAMPLE
secret_key: wJalrXUtnFEMI/K7MDENG
β Shared in Slack/Email:
"Hey, use these keys for the dev environment:
Access Key: AKIAIOSFODNN7EXAMPLE
Secret: wJalrXUtnFEMI/K7MDENG"
The Real Cost:
- π° Financial: $47K average for crypto mining attacks
- π Security: Data breaches, compliance violations
- β° Time: Days to investigate and remediate
- π° Stress: Explaining to management why this happened
- π Reputation: Customer trust damaged
Sound familiar? Let's fix this permanently.
π What are IAM Roles? (Quick Theory)
Simple Definition
IAM Role = Temporary credentials that AWS services can assume. Think of it as:
- A badge that proves who you are
- Automatically renewed every hour
- Can't be stolen because it's not stored anywhere
- Specific to one service or resource
IAM Policy = List of permissions. What you're allowed to do.
Think of it like this:
Access Keys (Bad):
Developer β Creates keys β Puts in code β Git β
Everyone has keys β π₯ Leaked β Attacker uses them
IAM Roles (Good):
EC2 Instance β Assumes role β AWS gives temporary credentials β
Uses them for 1 hour β Automatically renewed β
Can't be stolen (not stored anywhere) β
Why You Need This
Scenario 1: Developer Leaves Company
- Access Keys: They still have keys, can access everything
- IAM Roles: Revoke their IAM access, all roles stop working instantly
- Result: Zero risk
Scenario 2: Server Compromised
- Access Keys: Attacker steals keys, uses them forever
- IAM Roles: Temporary credentials expire in 1 hour, limited damage
- Result: Contained breach
Scenario 3: Compliance Audit
- Access Keys: "Who used this key?" β No idea
- IAM Roles: CloudTrail shows exactly who did what, when
- Result: Pass audit
IAM Components
1. Role - Identity that can be assumed
2. Policy - Permissions (what you can do)
3. Trust Policy - Who can assume the role
4. Instance Profile - Attaches role to EC2
Example:
EC2 Instance β Instance Profile β IAM Role β IAM Policy β S3 Access β
EC2 Instance β Instance Profile β IAM Role β IAM Policy β S3 Access β
π Prerequisites
Before starting, make sure you have:
- β Completed Article 8: Lambda Serverless
- β Terraform installed (v1.0+)
- β AWS CLI configured
- β Basic understanding of AWS permissions
- β SSH key pair in AWS
ποΈ What We're Building
EC2 Instance
β (IAM Role)
βββ S3 Bucket (Read/Write)
βββ CloudWatch (Metrics/Logs)
βββ SSM (Session Manager)
Lambda Function
β (IAM Role)
βββ S3 Bucket (Read Only)
Components:
- IAM Roles - Identity for services
- IAM Policies - Permissions
- Instance Profile - Attaches role to EC2
- S3 Bucket Policy - Resource-based access
- Managed Policies - AWS pre-built policies
- Custom Policies - Your specific permissions
π Project Structure
09-iam-security/
βββ main.tf
βββ variables.tf
βββ outputs.tf
βββ terraform.tfvars
π§ Step 1: Define Variables
Create variables.tf:
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Project name"
type = string
default = "terraform-iam"
}
variable "environment" {
description = "Environment"
type = string
default = "dev"
}
variable "s3_bucket_name" {
description = "S3 bucket name (must be globally unique)"
type = string
default = "terraform-iam-demo-bucket-2026"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "key_name" {
description = "SSH key pair name"
type = string
default = "my-key"
}
variable "my_ip" {
description = "Your IP for SSH"
type = string
default = "0.0.0.0/0"
}
πͺ£ Step 2: Create S3 Bucket with Policy
Add to main.tf:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# S3 Bucket
resource "aws_s3_bucket" "app_bucket" {
bucket = var.s3_bucket_name
force_destroy = true
tags = {
Name = var.s3_bucket_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "aws_s3_bucket_versioning" "app_bucket" {
bucket = aws_s3_bucket.app_bucket.id
versioning_configuration {
status = "Enabled"
}
}
# S3 Bucket Policy
resource "aws_s3_bucket_policy" "app_bucket" {
bucket = aws_s3_bucket.app_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEC2RoleAccess"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.ec2_role.arn
}
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app_bucket.arn,
"${aws_s3_bucket.app_bucket.arn}/*"
]
},
{
Sid = "DenyUnencryptedUploads"
Effect = "Deny"
Principal = "*"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.app_bucket.arn}/*"
Condition = {
StringNotEquals = {
"s3:x-amz-server-side-encryption" = "AES256"
}
}
}
]
})
}
Understanding Bucket Policy:
Statement 1: Allow EC2 Role
{
"Principal": { "AWS": "arn:aws:iam::123456789012:role/ec2-role" },
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": ["arn:aws:s3:::bucket/*"]
}
Allows our EC2 role to read/write objects.
Statement 2: Enforce Encryption
{
"Effect": "Deny",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
Blocks unencrypted uploads. Security best practice!
π€ Step 3: Create IAM Role for EC2
Add to main.tf:
# IAM Role for EC2
resource "aws_iam_role" "ec2_role" {
name = "${var.project_name}-ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
tags = {
Name = "${var.project_name}-ec2-role"
}
}
Assume Role Policy:
{
"Principal": {
"Service": "ec2.amazonaws.com"
}
}
This says: "EC2 service can assume this role"
Key Concepts:
- Role = Identity (who)
- Assume Role Policy = Trust policy (who can use this role)
- Permissions Policy = What the role can do
π Step 4: Create Custom IAM Policy
Add to main.tf:
# Custom IAM Policy for S3 Access
resource "aws_iam_policy" "s3_access" {
name = "${var.project_name}-s3-access"
description = "Allow S3 access for application"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app_bucket.arn,
"${aws_s3_bucket.app_bucket.arn}/*"
]
}
]
})
}
# Attach S3 Policy to EC2 Role
resource "aws_iam_role_policy_attachment" "ec2_s3" {
role = aws_iam_role.ec2_role.name
policy_arn = aws_iam_policy.s3_access.arn
}
Policy Structure:
{
"Effect": "Allow", // Allow or Deny
"Action": ["s3:GetObject"], // What actions
"Resource": ["arn:..."] // On which resources
}
Resource ARN Patterns:
-
arn:aws:s3:::bucket- The bucket itself -
arn:aws:s3:::bucket/*- Objects in the bucket
Least Privilege:
- Only allows specific actions
- Only on specific bucket
- No wildcard permissions
π Step 5: Attach AWS Managed Policies
Add to main.tf:
# Attach CloudWatch Policy
resource "aws_iam_role_policy_attachment" "ec2_cloudwatch" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
# Attach SSM Policy (for Session Manager)
resource "aws_iam_role_policy_attachment" "ec2_ssm" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
name = "${var.project_name}-ec2-profile"
role = aws_iam_role.ec2_role.name
}
AWS Managed Policies:
- Pre-built by AWS
- Maintained and updated by AWS
- Follow best practices
- Use when possible!
Common Managed Policies:
-
CloudWatchAgentServerPolicy- Send metrics/logs -
AmazonSSMManagedInstanceCore- Session Manager access -
AmazonS3ReadOnlyAccess- Read S3 -
AWSLambdaBasicExecutionRole- Lambda logging
Instance Profile:
Attaches IAM role to EC2 instance. Required!
π Step 6: Create Lambda IAM Role
Add to main.tf:
# IAM Role for Lambda
resource "aws_iam_role" "lambda_role" {
name = "${var.project_name}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
tags = {
Name = "${var.project_name}-lambda-role"
}
}
# Lambda Basic Execution Policy
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Custom Lambda Policy for S3 Read-Only
resource "aws_iam_policy" "lambda_s3_read" {
name = "${var.project_name}-lambda-s3-read"
description = "Allow Lambda to read from S3"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app_bucket.arn,
"${aws_s3_bucket.app_bucket.arn}/*"
]
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_s3" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.lambda_s3_read.arn
}
Different Assume Role Policy:
{
"Principal": {
"Service": "lambda.amazonaws.com" // Lambda, not EC2!
}
}
Least Privilege for Lambda:
- Only
GetObjectandListBucket - No
PutObjectorDeleteObject - Read-only access
π₯οΈ Step 7: Deploy EC2 with IAM Role
Add to main.tf:
# Security Group for EC2
resource "aws_security_group" "ec2" {
name = "${var.project_name}-ec2-sg"
description = "Security group for EC2"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-ec2-sg"
}
}
# EC2 Instance
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_name
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
vpc_security_group_ids = [aws_security_group.ec2.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y aws-cli
# Create test file
echo "Hello from Terraform IAM Demo" > /home/ec2-user/test.txt
chown ec2-user:ec2-user /home/ec2-user/test.txt
# Create helper scripts
cat > /home/ec2-user/test-s3.sh <<'SCRIPT'
#!/bin/bash
echo "Testing S3 Access..."
# List bucket
echo "1. Listing bucket contents:"
aws s3 ls s3://${var.s3_bucket_name}/
# Upload file
echo "2. Uploading test file:"
aws s3 cp /home/ec2-user/test.txt s3://${var.s3_bucket_name}/test.txt
# Download file
echo "3. Downloading file:"
aws s3 cp s3://${var.s3_bucket_name}/test.txt /home/ec2-user/downloaded.txt
echo "S3 test complete!"
SCRIPT
chmod +x /home/ec2-user/test-s3.sh
chown ec2-user:ec2-user /home/ec2-user/test-s3.sh
# Create IAM info script
cat > /home/ec2-user/show-iam-info.sh <<'SCRIPT'
#!/bin/bash
echo "=== IAM Role Information ==="
echo "Instance Profile:"
curl -s http://169.254.169.254/latest/meta-data/iam/info | jq .
echo -e "\n=== Current IAM Identity ==="
aws sts get-caller-identity
echo -e "\n=== Attached Policies ==="
ROLE_NAME=$(aws sts get-caller-identity --query Arn --output text | cut -d'/' -f2)
aws iam list-attached-role-policies --role-name $ROLE_NAME 2>/dev/null || echo "Cannot list policies (expected)"
SCRIPT
chmod +x /home/ec2-user/show-iam-info.sh
chown ec2-user:ec2-user /home/ec2-user/show-iam-info.sh
EOF
tags = {
Name = "${var.project_name}-instance"
Environment = var.environment
}
}
Key Line:
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
This attaches the IAM role to EC2!
User Data Scripts:
-
test-s3.sh- Test S3 permissions -
show-iam-info.sh- Show IAM role info
π€ Step 8: Define Outputs
Create outputs.tf:
# S3 Bucket Output
output "s3_bucket_name" {
description = "S3 bucket name"
value = aws_s3_bucket.app_bucket.id
}
output "s3_bucket_arn" {
description = "S3 bucket ARN"
value = aws_s3_bucket.app_bucket.arn
}
# IAM Role Outputs
output "ec2_role_name" {
description = "EC2 IAM role name"
value = aws_iam_role.ec2_role.name
}
output "ec2_role_arn" {
description = "EC2 IAM role ARN"
value = aws_iam_role.ec2_role.arn
}
output "lambda_role_name" {
description = "Lambda IAM role name"
value = aws_iam_role.lambda_role.name
}
# EC2 Output
output "ec2_public_ip" {
description = "EC2 public IP"
value = aws_instance.app.public_ip
}
output "ec2_instance_id" {
description = "EC2 instance ID"
value = aws_instance.app.id
}
# Test Commands
output "test_commands" {
description = "Commands to test IAM permissions"
value = <<-EOT
SSH to EC2:
ssh -i ${var.key_name}.pem ec2-user@${aws_instance.app.public_ip}
Test S3 access:
aws s3 ls s3://${aws_s3_bucket.app_bucket.id}
aws s3 cp test.txt s3://${aws_s3_bucket.app_bucket.id}/
Test CloudWatch:
aws cloudwatch put-metric-data --namespace TestApp --metric-name TestMetric --value 1
EOT
}
βοΈ Step 9: Set Variable Values
Create terraform.tfvars:
aws_region = "us-east-1"
project_name = "terraform-iam"
environment = "dev"
# Change this to a unique bucket name
s3_bucket_name = "terraform-iam-demo-bucket-2026"
instance_type = "t2.micro"
key_name = "my-key"
my_ip = "0.0.0.0/0"
Important: Change s3_bucket_name to something unique!
π Step 10: Deploy Infrastructure
1. Initialize Terraform
terraform init
2. Review Plan
terraform plan
You should see:
Plan: 13 to add, 0 to change, 0 to destroy.
3. Apply Configuration
terraform apply
Type yes when prompted.
β Step 11: Test IAM Permissions
1. Get EC2 IP
EC2_IP=$(terraform output -raw ec2_public_ip)
echo $EC2_IP
2. SSH to EC2
ssh -i my-key.pem ec2-user@$EC2_IP
3. Check IAM Role
./show-iam-info.sh
Output:
{
"Code": "Success",
"InstanceProfileArn": "arn:aws:iam::123456789012:instance-profile/terraform-iam-ec2-profile",
"InstanceProfileId": "AIPAI...",
"LastUpdated": "2026-03-24T16:00:00Z"
}
{
"UserId": "AROAI...:i-0123456789abcdef0",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/terraform-iam-ec2-role/i-0123456789abcdef0"
}
Notice: No access keys! Using IAM role.
4. Test S3 Access
./test-s3.sh
Output:
Testing S3 Access...
1. Listing bucket contents:
(empty or shows files)
2. Uploading test file:
upload: ./test.txt to s3://terraform-iam-demo-bucket-2026/test.txt
3. Downloading file:
download: s3://terraform-iam-demo-bucket-2026/test.txt to ./downloaded.txt
S3 test complete!
5. Test CloudWatch Access
aws cloudwatch put-metric-data \
--namespace TestApp \
--metric-name TestMetric \
--value 1
Should succeed (no output = success).
6. Test Denied Action
# Try to list all S3 buckets (should fail)
aws s3 ls
Output:
An error occurred (AccessDenied) when calling the ListBuckets operation
Perfect! Least privilege working - can only access our specific bucket.
π Understanding IAM Components
IAM Role vs IAM User
IAM User:
- Permanent identity
- Has access keys
- For humans or long-term apps
IAM Role:
- Temporary identity
- No access keys
- For services (EC2, Lambda)
- Credentials rotate automatically
Trust Policy vs Permissions Policy
Trust Policy (Assume Role):
{
"Principal": { "Service": "ec2.amazonaws.com" }
}
WHO can use this role.
Permissions Policy:
{
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::bucket/*"]
}
WHAT the role can do.
Identity-Based vs Resource-Based
Identity-Based (IAM Policy):
resource "aws_iam_policy" "s3_access" {
policy = jsonencode({
Action = ["s3:GetObject"]
Resource = ["arn:aws:s3:::bucket/*"]
})
}
Attached to identity (role/user).
Resource-Based (Bucket Policy):
resource "aws_s3_bucket_policy" "bucket" {
policy = jsonencode({
Principal = { AWS = "arn:aws:iam::123:role/my-role" }
Action = ["s3:GetObject"]
})
}
Attached to resource (bucket).
When to use which:
- Identity-based: Default choice
- Resource-based: Cross-account access, specific resource control
π§ͺ Advanced Testing
Test Policy Evaluation
# SSH to EC2
ssh -i my-key.pem ec2-user@$EC2_IP
# Test allowed actions
aws s3 cp test.txt s3://YOUR-BUCKET/test.txt # β
Should work
aws s3 ls s3://YOUR-BUCKET/ # β
Should work
# Test denied actions
aws s3 ls # β Should fail
aws s3 rm s3://OTHER-BUCKET/file.txt # β Should fail
aws ec2 describe-instances # β Should fail
View CloudTrail Logs
# From local machine
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=terraform-iam-ec2-role \
--max-results 10
Shows all actions performed by the role!
Test Session Manager (No SSH Keys!)
# From local machine
aws ssm start-session --target $(terraform output -raw ec2_instance_id)
Connect without SSH keys - using IAM role!
π Troubleshooting
Issue 1: "Access Denied" on S3
Cause: IAM policy or bucket policy issue.
Solution:
# Check role is attached
aws ec2 describe-instances \
--instance-ids <INSTANCE_ID> \
--query 'Reservations[0].Instances[0].IamInstanceProfile'
# Check policies attached to role
aws iam list-attached-role-policies \
--role-name terraform-iam-ec2-role
Issue 2: "No credentials found"
Cause: Instance profile not attached.
Solution:
# Ensure this line exists in aws_instance
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
Issue 3: Bucket policy conflicts
Error:
Error: error putting S3 Bucket Policy: MalformedPolicy
Solution: Check JSON syntax:
# Validate JSON
echo '{"Version":"2012-10-17"}' | jq .
Issue 4: Can't assume role
Error:
User: arn:aws:sts::123:assumed-role/role/i-xxx is not authorized to perform: sts:AssumeRole
Solution: Check assume role policy allows the service:
{
"Principal": {
"Service": "ec2.amazonaws.com" // Must match service
}
}
π° Cost Breakdown
Monthly Costs:
| Resource | Cost |
|---|---|
| IAM Roles | $0.00 (Free) |
| IAM Policies | $0.00 (Free) |
| S3 Bucket | $0.00 (Free tier) |
| t2.micro EC2 | $8.50 or Free tier |
| Total | ~$0-8.50/month |
IAM is FREE!
- No charge for roles
- No charge for policies
- No charge for users (up to limits)
π Security Best Practices
β What We Did Right
-
No Access Keys
- Used IAM roles instead
- Automatic credential rotation
-
Least Privilege
- Only necessary permissions
- Specific resources only
-
Separate Roles
- Different roles for EC2 and Lambda
- Each with minimum permissions
-
Resource-Based Policies
- Bucket policy enforces encryption
- Defense in depth
-
Managed Policies
- Used AWS-maintained policies
- Automatically updated
π Additional Recommendations
1. Use Policy Conditions:
policy = jsonencode({
Statement = [{
Condition = {
IpAddress = {
"aws:SourceIp" = "203.0.113.0/24"
}
}
}]
})
2. Enable MFA for Sensitive Actions:
Condition = {
BoolIfExists = {
"aws:MultiFactorAuthPresent" = "true"
}
}
3. Use Permission Boundaries:
resource "aws_iam_role" "limited" {
permissions_boundary = aws_iam_policy.boundary.arn
}
4. Regular Audits:
# List unused roles
aws iam get-credential-report
# Check last used
aws iam get-role --role-name my-role
5. Use SCPs (Service Control Policies):
Organization-level restrictions.
π§Ή Cleanup
terraform destroy
Type yes when prompted.
What gets deleted:
- β EC2 Instance
- β S3 Bucket (if empty)
- β IAM Roles
- β IAM Policies
- β Security Group
Note: If S3 bucket has objects, delete them first:
aws s3 rm s3://YOUR-BUCKET --recursive
π Key Concepts Learned
1. IAM Roles
resource "aws_iam_role" "role" {
assume_role_policy = jsonencode({...})
}
2. IAM Policies
resource "aws_iam_policy" "policy" {
policy = jsonencode({...})
}
3. Policy Attachments
resource "aws_iam_role_policy_attachment" "attach" {
role = aws_iam_role.role.name
policy_arn = aws_iam_policy.policy.arn
}
4. Instance Profiles
resource "aws_iam_instance_profile" "profile" {
role = aws_iam_role.role.name
}
5. Bucket Policies
resource "aws_s3_bucket_policy" "policy" {
policy = jsonencode({...})
}
π― Real-World Applications
1. Web Applications
- EC2 with S3 access for uploads
- RDS access for database
- CloudWatch for logging
2. Data Processing
- Lambda reading from S3
- Writing to DynamoDB
- SNS notifications
3. CI/CD Pipelines
- CodeBuild with ECR access
- CodeDeploy with EC2 access
- S3 for artifacts
4. Microservices
- Each service with own role
- Least privilege per service
- Service-to-service auth
π What's Next?
In the next article, we'll add:
- RDS Database - Managed PostgreSQL/MySQL
- Secrets Manager - Secure credential storage
- Private Subnets - Database isolation
- Security Groups - Network-level access control
Coming Up: Article 10: RDS + Secrets Manager with Terraform
π Additional Resources
π Source Code
The complete source code and Terraform configuration used in this article can be found on GitHub:
simplynadaf
/
terraform-by-sarvar
Terraform Series
π Terraform By Sarvar
Complete Terraform tutorial series from basics to advanced concepts
π Series Overview
This repository contains ONLY Terraform code examples for the Terraform By Sarvar tutorial series.
β οΈ IMPORTANT: This repo contains only.tffiles and infrastructure code. Articles are published on dev.to, not stored here.
π Read the series on dev.to: https://dev.to/sarvar_04/series/36963
π― What You'll Learn
- β Infrastructure as Code (IaC) fundamentals
- β Terraform basics to advanced concepts
- β AWS resource provisioning
- β Best practices and real-world patterns
- β Production-ready configurations
π Series Progress
π Article Series
π Foundation (Articles 1-5) β Complete
- Introduction to Terraform & IaC - β Published
- Installation & Setup - β Published
- Your First AWS Resource - β Published
- Understanding Terraform State - β Published
- Variables and Outputs - β Published
π Real Infrastructure (Articles 6-10)
Getting Started
Follow these steps to run the project on your local machine:
- Clone the repository:
git clone https://github.com/simplynadaf/terraform-by-sarvar.git
- Navigate to the project directory:
cd terraform-by-sarvar/articles/09-iam-security
β Summary
Today you learned:
- β Create IAM roles and policies
- β Implement least privilege access
- β Use instance profiles for EC2
- β Attach managed and custom policies
- β Configure bucket policies
- β Avoid access keys completely
- β Test and verify permissions
- β Follow security best practices
You now understand AWS IAM security! π
π Connect & Share
If you found this helpful:
- β Star the GitHub repo
- π¬ Leave a comment below
- π Share with your network
- π§ Subscribe for updates
Next Article: Secure Database Deployment with RDS + Secrets Manager
Written by: Sarvar Nadaf
Series: Terraform By Sarvar
Part: 9 of 20
Tags: #Terraform #AWS #DevOps #IaC #IAM #Security #CloudComputing
π Wrapping Up
Thank you for reading. I hope this article provided practical insights and a clearer understanding of the topic.
If you found this useful:
- β€οΈ Like if it added value
- π¦ Unicorn if youβre applying it today
- πΎ Save it for your next optimization session
- π Share it with your team
π‘ Whatβs Next
More deep dives are coming soon on:
- Cloud Operations
- GenAI & Agentic AI
- DevOps Automation
- Data & Platform Engineering
Follow along for weekly insights and hands-on guides.
π Portfolio & Work
You can explore my full body of work, certifications, architecture projects, and technical articles here:
π Visit My Website
π οΈ Services I Offer
If you're looking for hands-on guidance or collaboration, I provide:
- Cloud Architecture Consulting (AWS / Azure)
- DevSecOps & Automation Design
- FinOps Optimization Reviews
- Technical Writing (Cloud, DevOps, GenAI)
- Product & Architecture Reviews
- Mentorship & 1:1 Technical Guidance
π€ Letβs Connect
Iβd love to hear your thoughts. Feel free to drop a comment or connect with me on:
π LinkedIn
For collaborations, consulting, or technical discussions, reach out at:
Found this helpful? Share it with your team.
β Star the repo β’ π Follow the series β’ π¬ Ask questions
Made by Sarvar Nadaf
π https://sarvarnadaf.com
Top comments (0)