DeployU
Interviews / Cloud & DevOps / IAM policies are too permissive. Implement least privilege access with proper role design.

IAM policies are too permissive. Implement least privilege access with proper role design.

practical IAM & Security Interactive Quiz Code Examples

The Scenario

Your AWS security audit revealed issues:

Audit Findings:
├── 5 IAM users with AdministratorAccess
├── Lambda execution role: AmazonDynamoDBFullAccess
├── EC2 instances: Full S3 access (s3:*)
├── No MFA enforcement
├── Access keys older than 90 days: 12
├── Unused IAM roles: 23
└── Cross-account access: Unclear trust policies

The Challenge

Implement least privilege IAM policies, proper role design, and security best practices to pass compliance audits.

Wrong Approach

A junior engineer might use AWS managed policies for everything, grant full service access, skip conditions, or create one role for all resources. These approaches violate least privilege, increase blast radius, miss security controls, and make auditing difficult.

Right Approach

A senior engineer creates resource-specific policies with conditions, uses IAM Access Analyzer to find unused permissions, implements permission boundaries, enforces MFA, and designs roles following the separation of duties principle.

Step 1: Analyze Current Permissions

# Find overly permissive policies
aws iam list-policies --scope Local --query 'Policies[*].[PolicyName,Arn]'

# Check policy for wildcards
aws iam get-policy-version \
  --policy-arn arn:aws:iam::123456789:policy/MyPolicy \
  --version-id v1 \
  --query 'PolicyVersion.Document'

# Find unused roles
aws iam list-roles --query 'Roles[?contains(RoleName, `unused`)]'

# Use IAM Access Analyzer
aws accessanalyzer create-analyzer \
  --analyzer-name account-analyzer \
  --type ACCOUNT

# Generate policy based on CloudTrail
aws accessanalyzer generate-policy \
  --policy-generation-details principalArn=arn:aws:iam::123456789:role/LambdaRole

Step 2: Lambda Execution Role (Least Privilege)

# BEFORE: Overly permissive
resource "aws_iam_role_policy_attachment" "bad_example" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"  # Too broad!
}

# AFTER: Least privilege
resource "aws_iam_role" "lambda_orders" {
  name = "orders-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "lambda_orders" {
  name = "orders-lambda-policy"
  role = aws_iam_role.lambda_orders.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # CloudWatch Logs - only for this function
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:us-east-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/process-orders:*"
      },
      # DynamoDB - specific table, specific actions
      {
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:Query"
        ]
        Resource = [
          aws_dynamodb_table.orders.arn,
          "${aws_dynamodb_table.orders.arn}/index/*"
        ]
        # Condition: Only allow from VPC
        Condition = {
          StringEquals = {
            "aws:SourceVpc" = aws_vpc.main.id
          }
        }
      },
      # SQS - specific queue, limited actions
      {
        Effect = "Allow"
        Action = [
          "sqs:ReceiveMessage",
          "sqs:DeleteMessage",
          "sqs:GetQueueAttributes"
        ]
        Resource = aws_sqs_queue.orders.arn
      },
      # Secrets Manager - specific secret
      {
        Effect = "Allow"
        Action = "secretsmanager:GetSecretValue"
        Resource = aws_secretsmanager_secret.db_password.arn
      },
      # KMS - for decryption
      {
        Effect = "Allow"
        Action = "kms:Decrypt"
        Resource = aws_kms_key.app.arn
        Condition = {
          StringEquals = {
            "kms:EncryptionContext:SecretARN" = aws_secretsmanager_secret.db_password.arn
          }
        }
      }
    ]
  })
}

Step 3: EC2 Instance Role

resource "aws_iam_role" "ec2_app" {
  name = "app-server-role"

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

resource "aws_iam_role_policy" "ec2_app" {
  name = "app-server-policy"
  role = aws_iam_role.ec2_app.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # S3 - specific bucket, specific prefix
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject"
        ]
        Resource = "arn:aws:s3:::${aws_s3_bucket.app_data.id}/uploads/*"
      },
      {
        Effect = "Allow"
        Action = "s3:ListBucket"
        Resource = aws_s3_bucket.app_data.arn
        Condition = {
          StringLike = {
            "s3:prefix" = ["uploads/*"]
          }
        }
      },
      # Parameter Store - specific path
      {
        Effect = "Allow"
        Action = [
          "ssm:GetParameter",
          "ssm:GetParameters",
          "ssm:GetParametersByPath"
        ]
        Resource = "arn:aws:ssm:us-east-1:${data.aws_caller_identity.current.account_id}:parameter/app/prod/*"
      },
      # CloudWatch - scoped to application
      {
        Effect = "Allow"
        Action = [
          "cloudwatch:PutMetricData"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "cloudwatch:namespace" = "OrdersApp"
          }
        }
      }
    ]
  })
}

# Instance profile
resource "aws_iam_instance_profile" "ec2_app" {
  name = "app-server-profile"
  role = aws_iam_role.ec2_app.name
}

Step 4: Permission Boundaries

# Permission boundary for all developer-created roles
resource "aws_iam_policy" "developer_boundary" {
  name = "DeveloperPermissionBoundary"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # Allow: Common development resources
      {
        Effect = "Allow"
        Action = [
          "lambda:*",
          "dynamodb:*",
          "sqs:*",
          "sns:*",
          "s3:*",
          "logs:*"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:RequestedRegion" = ["us-east-1", "us-west-2"]
          }
        }
      },
      # Deny: Security-sensitive actions
      {
        Effect   = "Deny"
        Action   = [
          "iam:*",
          "organizations:*",
          "account:*"
        ]
        Resource = "*"
      },
      # Deny: Production resources
      {
        Effect = "Deny"
        Action = "*"
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:ResourceTag/Environment" = "production"
          }
        }
      },
      # Deny: Creating IAM users (force roles)
      {
        Effect   = "Deny"
        Action   = [
          "iam:CreateUser",
          "iam:CreateAccessKey"
        ]
        Resource = "*"
      }
    ]
  })
}

# Apply boundary to developer role
resource "aws_iam_role" "developer" {
  name = "developer-role"

  permissions_boundary = aws_iam_policy.developer_boundary.arn

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
      }
      Condition = {
        Bool = {
          "aws:MultiFactorAuthPresent" = "true"
        }
      }
    }]
  })
}

Step 5: Cross-Account Access

# Role in Account B that Account A can assume
resource "aws_iam_role" "cross_account" {
  name = "cross-account-deploy-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::111111111111:role/cicd-role"
      }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "sts:ExternalId" = "unique-external-id-12345"
        }
      }
    }]
  })
}

# Policy for what Account A can do in Account B
resource "aws_iam_role_policy" "cross_account" {
  name = "cross-account-deploy-policy"
  role = aws_iam_role.cross_account.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "lambda:UpdateFunctionCode",
          "lambda:UpdateFunctionConfiguration"
        ]
        Resource = "arn:aws:lambda:us-east-1:222222222222:function:*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:PutObject"
        ]
        Resource = "arn:aws:s3:::deploy-artifacts-222222222222/*"
      }
    ]
  })
}

Step 6: Service Control Policies (Organizations)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    },
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    },
    {
      "Sid": "RequireEncryption",
      "Effect": "Deny",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "*",
      "Condition": {
        "Null": {
          "s3:x-amz-server-side-encryption": "true"
        }
      }
    },
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "RestrictRegions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "us-east-1",
            "us-west-2"
          ]
        },
        "ForAnyValue:StringNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/OrganizationAccountAccessRole"
          ]
        }
      }
    }
  ]
}

Step 7: Enforce MFA

# Policy requiring MFA for sensitive actions
resource "aws_iam_policy" "require_mfa" {
  name = "RequireMFAForSensitiveActions"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # Allow basic read actions without MFA
      {
        Effect = "Allow"
        Action = [
          "iam:GetAccountPasswordPolicy",
          "iam:GetAccountSummary",
          "iam:ListMFADevices"
        ]
        Resource = "*"
      },
      # Allow managing own MFA
      {
        Effect = "Allow"
        Action = [
          "iam:CreateVirtualMFADevice",
          "iam:EnableMFADevice",
          "iam:ResyncMFADevice"
        ]
        Resource = [
          "arn:aws:iam::*:mfa/$${aws:username}",
          "arn:aws:iam::*:user/$${aws:username}"
        ]
      },
      # Deny everything else without MFA
      {
        Effect = "Deny"
        NotAction = [
          "iam:CreateVirtualMFADevice",
          "iam:EnableMFADevice",
          "iam:GetAccountPasswordPolicy",
          "iam:GetAccountSummary",
          "iam:ListMFADevices",
          "iam:ResyncMFADevice",
          "sts:GetSessionToken"
        ]
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultiFactorAuthPresent" = "false"
          }
        }
      }
    ]
  })
}

IAM Security Checklist

PrincipleImplementationPurpose
Least privilegeSpecific resources + actionsLimit blast radius
No wildcardsExplicit ARNsPredictable access
ConditionsVPC, MFA, tagsContext-aware security
Permission boundariesLimit delegationPrevent privilege escalation
Regular reviewIAM Access AnalyzerRemove unused permissions

Policy Evaluation Order

1. Explicit Deny in any policy → DENY
2. SCP Deny → DENY
3. Permission Boundary Deny → DENY
4. Session Policy Deny → DENY
5. Identity Policy Allow? → Check Resource Policy
6. Resource Policy Allow? → ALLOW
7. Default → DENY

Practice Question

Why should you use IAM roles instead of IAM users with access keys for applications?