IAM: The Identity Layer
IAM is the most critical security layer on AWS. Every API call, whether from a user, application, or service, must be authenticated and authorized through IAM. A misconfigured IAM policy can render every other security control irrelevant. Getting IAM right is the single highest-impact thing you can do for your AWS security posture.
Core Concepts​
Principals​
A principal is any entity that can make a request to AWS. Principals include:
- IAM Users - Long-lived identities with permanent credentials. Use sparingly and only when federated access is not possible.
- IAM Roles - Identities that are assumed temporarily. Preferred over users for applications and services.
- AWS Services - Services like Lambda, EC2, and ECS that assume roles to interact with other services.
- Federated Users - External identities (SSO, SAML, OIDC) that assume roles through federation.
Policies​
Policies are JSON documents that define permissions. There are several types:
| Policy Type | Attached To | Purpose |
|---|---|---|
| Identity-based | Users, groups, roles | Grant permissions to the principal |
| Resource-based | S3 buckets, KMS keys, SQS queues | Grant cross-account or service access to the resource |
| Permission boundaries | Users, roles | Set the maximum permissions a principal can have |
| Service control policies (SCPs) | AWS Organizations OUs | Set the maximum permissions for all accounts in an OU |
| Session policies | STS AssumeRole calls | Limit permissions for a specific session |
Roles and Trust Relationships​
An IAM role has two key components:
- Trust policy - Defines who can assume the role (the principal). This is a resource-based policy on the role itself.
- Permission policy - Defines what the role can do once assumed.
// Trust policy: who can assume this role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
The trust policy above allows the Lambda service to assume this role. Without a trust policy explicitly granting access, no one can assume the role, regardless of what permissions it has.
Least Privilege Principle​
Least privilege means granting only the exact permissions needed, nothing more. In practice:
- Scope actions: Use specific actions like
s3:GetObjectinstead ofs3:*. - Scope resources: Target specific ARNs instead of
"Resource": "*". - Scope conditions: Restrict by IP, time, MFA status, tags, or other context.
- Use time-limited credentials: Prefer roles (temporary credentials) over users (permanent credentials).
Practical Example: Too Broad vs Least Privilege​
Too broad (never do this):
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
Least privilege:
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-data-bucket/uploads/*"
}
The second policy limits the principal to reading and writing objects only in a specific prefix of a specific bucket.
IAM Policy Evaluation Logic​
When AWS evaluates whether to allow or deny a request, it follows this order:
- Explicit Deny - If any applicable policy has an explicit
"Effect": "Deny", the request is denied. Period. Explicit deny always wins. - Organizations SCPs - If the account is in an Organization, SCPs must allow the action (they do not grant access, they set the ceiling).
- Resource-based policies - If a resource-based policy grants access, the request may be allowed even without an identity-based policy (for same-account access).
- Permission boundaries - If set, the action must be allowed by both the permission boundary and the identity-based policy.
- Identity-based policies - The principal's attached policies must explicitly allow the action.
- Implicit Deny - If nothing explicitly allows the request, it is denied by default.
The key rule: Explicit Deny > Explicit Allow > Implicit Deny.
Common IAM Mistakes​
1. Wildcard Actions and Resources​
Using "Action": "*" or "Resource": "*" defeats the purpose of IAM. Every policy should specify the minimum actions and target specific resource ARNs.
2. No Conditions​
Policies without conditions miss an opportunity to add context-aware restrictions. Always consider whether you can restrict by source IP, VPC, time, tags, or MFA status.
3. Long-Lived Credentials​
IAM user access keys are permanent until rotated. Prefer IAM roles with temporary credentials from STS. If you must use access keys, enforce rotation policies and never embed them in code.
4. Overly Broad Trust Policies​
A trust policy with "Principal": {"AWS": "*"} allows any AWS account to assume the role. Always specify exact account IDs or service principals.
5. Not Using Permission Boundaries​
For teams that create their own roles (such as developers deploying with Terraform), permission boundaries prevent privilege escalation by capping the maximum permissions any created role can have.
Terraform: Lambda Execution Role with Least Privilege​
This creates an IAM role for a Lambda function that reads from a specific DynamoDB table and writes logs to CloudWatch.
- Terraform
- CDK (TypeScript)
- CDK (Python)
- CloudFormation
# IAM role for a Lambda function that reads from DynamoDB
# and writes to CloudWatch Logs
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
region = data.aws_region.current.name
}
resource "aws_iam_role" "order_processor_lambda" {
name = "order-processor-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"aws:SourceAccount" = local.account_id
}
}
}
]
})
tags = {
Application = "order-service"
ManagedBy = "terraform"
}
}
# Least-privilege policy: only the actions this function actually needs
resource "aws_iam_role_policy" "order_processor_permissions" {
name = "order-processor-permissions"
role = aws_iam_role.order_processor_lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadFromOrdersTable"
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:Query"
]
Resource = [
"arn:aws:dynamodb:${local.region}:${local.account_id}:table/orders",
"arn:aws:dynamodb:${local.region}:${local.account_id}:table/orders/index/*"
]
},
{
Sid = "WriteCloudWatchLogs"
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:${local.region}:${local.account_id}:log-group:/aws/lambda/order-processor:*"
}
]
})
}
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
Notice that the role specifies exact DynamoDB actions (GetItem, Query) on a specific table, and CloudWatch Logs permissions scoped to the function's own log group. The trust policy also includes a source account condition to prevent confused deputy attacks.
Terraform: IAM Policy with Conditions​
This policy restricts S3 access to requests that come from a specific VPC endpoint and require MFA.
- Terraform
- CDK (TypeScript)
- CDK (Python)
- CloudFormation
# IAM policy that restricts S3 access with conditions:
# - Must come through a specific VPC endpoint
# - Must have active MFA session
resource "aws_iam_policy" "restricted_s3_access" {
name = "restricted-s3-data-access"
description = "S3 access restricted to VPC endpoint and MFA"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowS3AccessFromVPCEndpointWithMFA"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::${var.data_bucket_name}",
"arn:aws:s3:::${var.data_bucket_name}/*"
]
Condition = {
# Only allow access through the VPC endpoint
StringEquals = {
"aws:sourceVpce" = var.vpc_endpoint_id
}
# Require MFA authentication
Bool = {
"aws:MultiFactorAuthPresent" = "true"
}
}
},
{
Sid = "DenyUnencryptedUploads"
Effect = "Deny"
Action = "s3:PutObject"
Resource = "arn:aws:s3:::${var.data_bucket_name}/*"
Condition = {
StringNotEquals = {
"s3:x-amz-server-side-encryption" = "aws:kms"
}
}
}
]
})
tags = {
SecurityLayer = "identity"
ManagedBy = "terraform"
}
}
variable "data_bucket_name" {
type = string
description = "Name of the S3 bucket to grant access to"
}
variable "vpc_endpoint_id" {
type = string
description = "VPC endpoint ID to restrict access through"
}
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
CDK & CloudFormation Templates
Get this implementation in AWS CDK (TypeScript/Python) and CloudFormation YAML.
This policy uses multiple conditions: the sourceVpce condition ensures requests only come through your private VPC endpoint, and the MFA condition ensures the user has authenticated with a second factor. The deny statement prevents any uploads that are not encrypted with KMS.
Flashcards​
What are the two components of an IAM role?
Click to reveal1) A trust policy that defines who can assume the role (the principal). 2) A permission policy that defines what the role can do once assumed.