AWS IAM Least-Privilege Design: Policy Patterns, Condition Keys, and Terraform
Design AWS IAM policies following least-privilege principles. Covers resource-level permissions, condition keys for MFA and region locking, role chaining, service-linked roles, permission boundaries, Terraform IAM modules, and common over-permissive patterns to avoid.
"Action": "*", "Resource": "*" — this line appears in AWS documentation examples and quickly finds its way into production policies where it was never meant to be permanent. Over-permissive IAM is one of the most common causes of cloud security incidents: a compromised Lambda function can read every secret, a misconfigured EC2 instance can exfiltrate all S3 data, a developer credential can delete production databases.
Least-privilege IAM is not about being paranoid — it's about ensuring that when something goes wrong, the blast radius is bounded.
The Least-Privilege Checklist
For every IAM role and policy:
- Actions: Only the specific actions needed (not wildcards unless truly required)
- Resources: Only the specific ARNs (not
*unless the service doesn't support resource-level permissions) - Conditions: Add where meaningful — region, MFA, VPC, time of day
- Review: Rotate and audit quarterly using AWS Access Analyzer
Application Roles: ECS Task
# terraform/iam/ecs-task-role.tf
# Task execution role: allows ECS to pull images + fetch secrets
resource "aws_iam_role" "ecs_execution" {
name = "${var.app_name}-ecs-execution-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution_managed" {
role = aws_iam_role.ecs_execution.name
# AWS-managed policy: ECR pull + CloudWatch logs + SSM parameter read
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Grant access to specific secrets only (not all secrets)
resource "aws_iam_role_policy" "ecs_execution_secrets" {
name = "secrets-access"
role = aws_iam_role.ecs_execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [
"arn:aws:secretsmanager:${var.aws_region}:${var.aws_account_id}:secret:${var.app_name}/${var.environment}/*"
]
}]
})
}
# Task role: what the application can DO at runtime
resource "aws_iam_role" "ecs_task" {
name = "${var.app_name}-ecs-task-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "ecs_task_app" {
name = "app-permissions"
role = aws_iam_role.ecs_task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# S3: access only app bucket, only specific prefixes
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
Resource = ["arn:aws:s3:::${var.app_name}-${var.environment}/uploads/*"]
},
{
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = ["arn:aws:s3:::${var.app_name}-${var.environment}"]
Condition = {
StringLike = { "s3:prefix" = ["uploads/*"] }
}
},
# SQS: access only specific queues
{
Effect = "Allow"
Action = ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
Resource = [
"arn:aws:sqs:${var.aws_region}:${var.aws_account_id}:${var.app_name}-*.fifo",
"arn:aws:sqs:${var.aws_region}:${var.aws_account_id}:${var.app_name}-*"
]
},
# SES: send email only from app domain
{
Effect = "Allow"
Action = ["ses:SendEmail", "ses:SendRawEmail"]
Resource = ["arn:aws:ses:${var.aws_region}:${var.aws_account_id}:identity/mail.viprasol.com"]
},
# CloudWatch: write logs + metrics
{
Effect = "Allow"
Action = ["logs:CreateLogStream", "logs:PutLogEvents", "cloudwatch:PutMetricData"]
Resource = ["*"] # CloudWatch doesn't support resource-level for these actions
}
]
})
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Lambda Execution Role
# terraform/iam/lambda-role.tf
resource "aws_iam_role" "lambda" {
name = "${var.function_name}-lambda-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
# Basic Lambda execution: CloudWatch logs only
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# VPC access (if Lambda runs in VPC)
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
count = var.vpc_enabled ? 1 : 0
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
# Function-specific permissions (principle of least privilege)
resource "aws_iam_role_policy" "lambda_app" {
name = "function-permissions"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# Only read from specific DynamoDB table + only specific attributes
{
Effect = "Allow"
Action = ["dynamodb:GetItem", "dynamodb:Query"]
Resource = ["arn:aws:dynamodb:${var.aws_region}:${var.aws_account_id}:table/${var.table_name}"]
},
# SSM Parameter Store: only specific paths
{
Effect = "Allow"
Action = ["ssm:GetParameter", "ssm:GetParameters"]
Resource = [
"arn:aws:ssm:${var.aws_region}:${var.aws_account_id}:parameter/${var.app_name}/${var.environment}/*"
]
}
]
})
}
Developer Roles with Condition Keys
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireMFAForSensitiveActions",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:DeleteUser",
"iam:AttachUserPolicy",
"iam:CreateAccessKey",
"ec2:TerminateInstances",
"rds:DeleteDBInstance",
"s3:DeleteBucket"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
},
{
"Sid": "LockToApprovedRegions",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "eu-west-1"]
},
"StringNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::ACCOUNT_ID:role/BreakGlassAdmin"
}
}
},
{
"Sid": "AllowS3AccessToOwnPrefix",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::company-dev-bucket/users/${aws:username}/*"
// ${aws:username} is replaced at evaluation time with the caller's IAM username
},
{
"Sid": "DenyProductionResourceModification",
"Effect": "Deny",
"Action": ["rds:DeleteDBInstance", "rds:ModifyDBInstance", "ec2:TerminateInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": "production"
}
}
}
]
}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Permission Boundaries
Permission boundaries cap the maximum permissions a role can have, even if its attached policies grant more. Critical for delegate-and-trust models where developers can create roles.
# terraform/iam/permission-boundary.tf
# The permission boundary: maximum what any app role can do
resource "aws_iam_policy" "app_permission_boundary" {
name = "${var.app_name}-permission-boundary"
description = "Maximum permissions for any role created by ${var.app_name} CI/CD"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# Allow access to app resources only
{
Effect = "Allow"
Action = [
"s3:*", "sqs:*", "sns:*",
"secretsmanager:GetSecretValue",
"ssm:GetParameter*",
"logs:*", "cloudwatch:PutMetricData",
"xray:*"
]
Resource = [
"arn:aws:s3:::${var.app_name}-*",
"arn:aws:sqs:*:${var.aws_account_id}:${var.app_name}-*",
"arn:aws:secretsmanager:*:${var.aws_account_id}:secret:${var.app_name}/*",
"arn:aws:ssm:*:${var.aws_account_id}:parameter/${var.app_name}/*",
"arn:aws:logs:*:${var.aws_account_id}:*",
]
},
# Explicitly deny IAM privilege escalation — can never create roles with more power
{
Effect = "Deny"
Action = ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy", "iam:PassRole"]
Resource = "*"
}
]
})
}
# Apply boundary when creating any app role
resource "aws_iam_role" "app_with_boundary" {
name = "${var.app_name}-app-role"
permissions_boundary = aws_iam_policy.app_permission_boundary.arn
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
AWS Access Analyzer: Find Over-Permissive Policies
# terraform/iam/access-analyzer.tf
# Enable Access Analyzer for the account (run once per region)
resource "aws_accessanalyzer_analyzer" "account" {
analyzer_name = "${var.app_name}-analyzer"
type = "ACCOUNT"
}
# Optional: archive known-safe findings
resource "aws_accessanalyzer_archive_rule" "internal_s3" {
analyzer_name = aws_accessanalyzer_analyzer.account.analyzer_name
rule_name = "internal-s3-access"
filter {
criteria = "resourceType"
eq = ["AWS::S3::Bucket"]
}
filter {
criteria = "principal.AWS"
contains = [var.aws_account_id] # Internal account access is expected
}
}
Common Over-Permissive Patterns to Fix
// ❌ PATTERN 1: Wildcard actions on a service
{
"Action": "s3:*", // Includes s3:DeleteBucket, s3:PutBucketPolicy
"Resource": "*"
}
// ✅ FIX: Only the actions your app actually needs
{
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
}
// ❌ PATTERN 2: PassRole on *
{
"Action": "iam:PassRole",
"Resource": "*" // Can pass any role — including AdministratorAccess
}
// ✅ FIX: Only pass specific roles
{
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::ACCOUNT_ID:role/my-ecs-task-role"
}
// ❌ PATTERN 3: Secrets Manager on *
{
"Action": "secretsmanager:GetSecretValue",
"Resource": "*" // Can read EVERY secret in the account
}
// ✅ FIX: Only app-specific secrets by path
{
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:my-app/production/*"
}
// ❌ PATTERN 4: No resource-level restrictions on RDS
{
"Action": ["rds:DeleteDBInstance", "rds:ModifyDBCluster"],
"Resource": "*"
}
// ✅ FIX: Tag-based restriction
// Add Condition: StringEquals aws:ResourceTag/Environment: "development"
Cost and Timeline Estimates
IAM is a configuration concern, not a runtime cost:
| Scope | Team | Timeline | Cost Range |
|---|---|---|---|
| IAM audit of existing roles | 1 cloud engineer | 1–2 days | $400–800 |
| Implement least-privilege for all app roles | 1 engineer | 1–2 weeks | $2,000–4,000 |
| + Permission boundaries + Access Analyzer | 1 engineer | 2–3 days | $600–1,200 |
| Full IAM Terraform module with CI/CD roles | 1 engineer | 1–2 weeks | $2,500–5,000 |
See Also
- AWS Secrets Manager and Parameter Store
- Terraform State Management
- AWS CloudTrail Audit Logging
- AWS WAF for Application Security
- AWS ECS Fargate Production Setup
Working With Viprasol
IAM mistakes are silent until they're catastrophic. Our team designs least-privilege IAM from day one — task roles with resource-level ARN restrictions, developer roles with MFA and region conditions, permission boundaries that prevent privilege escalation, and Access Analyzer to surface over-permissive policies before they become incidents.
What we deliver:
- ECS task role and execution role with resource-level S3, SQS, and Secrets Manager permissions
- Lambda execution role with function-specific permissions
- Developer roles with MFA-required conditions for sensitive actions
- Permission boundary to cap maximum role permissions (prevents escalation)
- Terraform IAM module with reusable patterns across environments
Talk to our team about your AWS security architecture →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.