Hands-on lab demonstrating least-privilege IAM design for two EC2 instances using two IAM roles and two customer-managed policies:
- READONLY: read-only access to a scoped S3 bucket (storage only)
- FULLACCESS: read/write access to the same S3 bucket + scoped DynamoDB access (storage + database)
Includes an automated validator that runs 16 AWS CLI test actions (allowed vs denied) and reports pass/fail.
flowchart LR
subgraph AWS_Account[AWS Account]
S3[(S3 Bucket)]
DDB[(DynamoDB Table)]
EC2A[EC2 Instance A<br/>Role: readonly-ec2]
EC2B[EC2 Instance B<br/>Role: full-ec2]
RO[(Customer-Managed Policy<br/>readonly-storage)]
FA[(Customer-Managed Policy<br/>full-storage-db)]
end
EC2A --> RO
EC2B --> FA
RO --> S3
FA --> S3
FA --> DDB
Access to instances is via AWS Systems Manager (SSM) (no inbound SSH required).
cd terraform
terraform init
terraform applyTerraform outputs:
- S3 bucket name
- DynamoDB table name
- EC2 instance IDs (readonly + full)
- SSM connect commands
From repo root:
./scripts/ssm_connect.sh <readonly_instance_id>
./scripts/ssm_connect.sh <full_instance_id>Readonly instance:
validate_access readonlyFull instance:
validate_access fullThe validate_access command is installed by user-data at /usr/local/bin/validate_access.
Templates in policies/templates/ show the tightening process:
- v1: intentionally broad (wildcards /
*resources) - v2: resource scoping introduced
- v3: action lists tightened
- v4: final least-privilege set attached by Terraform
Run metrics locally:
python3 scripts/policy_metrics.pycd terraform
terraform destroy