Lambda execution roles are the single biggest permission sprawl problem in serverless AWS environments. EC2 roles at least require someone to make a deliberate choice about what instance profile to attach. Lambda function creation often defaults to creating a new role with managed policies attached "just to get things working" — and those policies stick around indefinitely. AWS Lambda documentation examples frequently show AWSLambdaBasicExecutionRole plus a service's full-access policy, which is a sensible starting point for getting things working but a terrible production configuration for any function with real security implications.
This guide covers how to design least-privilege execution roles, use IAM Access Analyzer to generate minimum policies from actual usage, and build the processes that prevent role sprawl in the first place.
Role Per Function vs. Shared Roles
The most impactful organizational decision is whether to use a dedicated role per Lambda function or to share roles across functions with similar permissions. The tradeoffs:
Role per function: Each function has a role with exactly the permissions it needs. Blast radius of any individual function compromise is limited to that function's permissions. More roles to manage, but Infrastructure as Code makes role creation cheap. Required for production environments with security sensitivity.
Shared roles: Functions with similar permission profiles share a role. Fewer roles, less IAM overhead. But a compromised function inherits the permissions of all functions sharing its role. Acceptable for development environments, not for production functions handling sensitive data or performing privileged actions.
Start with role-per-function as the default and introduce role sharing only when you can verify that all functions sharing a role have the same trust requirements and sensitivity level.
Building Minimum Permission Policies
The standard process for building a least-privilege Lambda execution role:
Step 1: Start with the known requirements. List every AWS service the function interacts with — the services called in the function code and any services involved in the event processing pipeline (DynamoDB stream as a trigger, SQS as a destination, etc.). For each service, identify the specific API calls the function makes.
Step 2: Write a policy with specific actions and resource ARNs. Avoid wildcards in both the action and resource fields. dynamodb:GetItem on arn:aws:dynamodb:us-east-1:123456789012:table/UserSessions is precise. dynamodb:* on * is a shortcut that costs you security hygiene. Resource wildcards are sometimes necessary (for services where the specific resource ARN isn't known at policy creation time), but they should be documented and reviewed.
Step 3: Validate with IAM Access Analyzer. After deploying and running representative workloads, use Access Analyzer's policy generation feature to compare your manual policy against the actual API calls observed in CloudTrail. If Access Analyzer suggests additional permissions (calls you missed), add them. If it shows permissions you granted that were never called, evaluate whether they're needed for edge cases or can be removed.
The Access Analyzer policy generation is particularly valuable for discovering permissions you forgot — service calls made during initialization, error handling paths, or service-to-service calls the Lambda triggers internally.
AWS Managed Policies to Use (and Avoid)
Some AWS managed policies are appropriate for Lambda execution roles:
AWSLambdaBasicExecutionRole: allows writing to CloudWatch Logs. This is appropriate for almost all Lambda functions — all functions need to be able to write logs.
AWSLambdaVPCAccessExecutionRole: extends basic execution role with EC2 VPC attachment permissions needed for Lambda functions deployed in VPCs. Use this for VPC-deployed functions.
AWSXRayDaemonWriteAccess: allows sending trace data to X-Ray. Add this when X-Ray tracing is enabled.
Avoid managed policies like AmazonDynamoDBFullAccess, AmazonS3FullAccess, or service-level full-access policies for production Lambda functions. These grant far more permissions than any individual function needs. Create customer-managed policies with specific actions and resource ARNs instead.
Automating Role Creation with IaC
Infrastructure as Code for Lambda function creation should include the execution role definition as part of the function definition. In Terraform:
resource "aws_iam_role" "process_orders" {
name = "lambda-process-orders"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy" "process_orders" {
role = aws_iam_role.process_orders.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem"]
Resource = aws_dynamodb_table.orders.arn
},
{
Effect = "Allow"
Action = ["sqs:SendMessage"]
Resource = aws_sqs_queue.fulfillment.arn
}
]
})
}
This pattern creates a tightly scoped role defined alongside the function. Code review of Lambda function changes includes review of role permission changes — someone must explicitly approve new permissions before they're deployed.
Auditing Existing Lambda Roles
For existing Lambda deployments, audit current role permissions against actual usage. Use IAM Access Analyzer's unused access analyzer to identify roles that have granted permissions that haven't been used in the analyzer period. Roles with many unused permissions are candidates for policy tightening.
AWS CloudTrail provides the evidence — query CloudTrail for Lambda function invocations and the API calls made during those invocations, then compare against the role's permissions. Any permission in the role policy that doesn't appear in CloudTrail activity over a 30-day window is a candidate for removal. Exceptions: permissions needed for error paths, disaster recovery, and other low-frequency operations that may not appear in a 30-day sample.
Related Reading
- Lambda security overview — permissions in the context of full Lambda security
- IAM security best practices — foundational IAM principles that apply to Lambda roles
- IAM security monitoring — detecting permission misuse in Lambda execution
- CloudTrail analysis — using CloudTrail to validate Lambda permission scope
FAQ
Can I use resource-based policies instead of execution roles for Lambda permissions?
Lambda supports resource-based policies that control which services or accounts can invoke the function. This is separate from the execution role, which controls what the function can do. Both are needed: the resource-based policy governs invocation (who can call the function), and the execution role governs what the function can do once invoked. Resource-based policies don't replace execution roles — they're complementary controls.
How do I handle Lambda functions that need access to multiple DynamoDB tables?
List each table's ARN separately in the resource list for the DynamoDB actions. If the function always accesses the same tables, this is straightforward. If the tables are dynamic (selected at runtime based on event data), you'll need a broader resource scope — consider whether you can scope to a table name prefix rather than wildcarding all tables. arn:aws:dynamodb:us-east-1:123456789012:table/orders-* is more specific than * while still covering multiple tables.
Should Lambda roles have permission to read their own environment variables?
Lambda functions can access their own environment variables without IAM permissions — the execution environment provides them directly. The IAM permission lambda:GetFunctionConfiguration is what allows external principals to read environment variables via the API. Restrict who has this permission in your IAM policies to limit visibility into function configuration including environment variables.
Protect your AWS accounts before it's too late
Vigilare monitors your AWS accounts for suspension risks — billing anomalies, IAM issues, GuardDuty findings, and more — and alerts you before AWS takes action.
Written by Viktor B.
Co-founder & CEO