Interviews / Cloud & DevOps / IAM policies are too permissive. Implement least privilege access with proper role design.
Lambda functions are timing out when accessing RDS in a VPC. Debug the connectivity issue.
Design a multi-tier VPC architecture with public, private, and database subnets.
DynamoDB is throttling requests and costs are high. Optimize the table design.
RDS connections are exhausted and failover takes too long. Fix the database setup.
Implement S3 with CloudFront for secure, cached content delivery with signed URLs.
ECS tasks are failing with exit code 137 and health check failures. Debug the container issues.
Messages are being lost and processed multiple times. Implement reliable SQS/SNS messaging.
Design a scalable API Gateway with throttling, caching, and Lambda integration.
Production incidents take hours to detect. Implement CloudWatch alarms and dashboards.
IAM policies are too permissive. Implement least privilege access with proper role design.
Build a CI/CD pipeline with CodePipeline that deploys to ECS with blue-green deployments.
Your AWS bill increased 40% last month. Identify waste and implement cost controls.
Questions
IAM policies are too permissive. Implement least privilege access with proper role design.
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/LambdaRoleStep 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
| Principle | Implementation | Purpose |
|---|---|---|
| Least privilege | Specific resources + actions | Limit blast radius |
| No wildcards | Explicit ARNs | Predictable access |
| Conditions | VPC, MFA, tags | Context-aware security |
| Permission boundaries | Limit delegation | Prevent privilege escalation |
| Regular review | IAM Access Analyzer | Remove 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?