Fixing AWS Lambda Trust Relationship Error: Missing `lambda.amazonaws.com` Principal for Event Source Mapping
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: The IAM role attached to your Lambda function does not trust
lambda.amazonaws.comas a principal — the AWS service plane cannot assume the role, so event source mapping (SQS, Kinesis, DynamoDB Streams, Kafka) silently fails to invoke the function. - How to fix it: Add
"lambda.amazonaws.com"to thePrincipal.Servicelist in the role's trust policy (AssumeRolePolicyDocument), then re-save the role. - Fastest path: Use our Client-Side Sandbox above to auto-refactor this — paste your trust policy JSON and get a corrected, least-privilege version generated locally without sending your ARNs anywhere.
The Incident (What does the error mean?)
Raw error surfaced in CloudWatch or via aws lambda list-event-source-mappings:
The provided execution role does not have permissions to call
AssumeRole on itself. The trust relationship for principal
service 'lambda.amazonaws.com' is missing.
ErrorCode: InvalidParameterValueException
Or in EventBridge / ESM status:
{
"StateTransitionReason": "USER_INITIATED",
"State": "Disabled",
"LastProcessingResult": "PROBLEM: Function call failed"
}
Immediate consequence: The event source mapping is created (it shows up in the console), but the Lambda poller — the internal AWS-managed fleet that reads from SQS/Kinesis/DynamoDB Streams — cannot assume your execution role. No messages are consumed. No errors surface in your application. Your queue depth climbs silently.
The Attack Vector / Blast Radius
This is a silent availability failure, not a noisy crash. The blast radius depends on your workload:
| Event Source | Failure Mode | Data Risk |
|---|---|---|
| SQS Standard | Messages accumulate until DLQ or TTL | Message loss after visibility timeout cycles |
| SQS FIFO | Full queue block — no consumer | Ordering guarantees break downstream |
| Kinesis / DynamoDB Streams | Shard iterator expires after 7 days | Permanent data loss — records are gone |
| MSK / Kafka | Consumer group lag grows unbounded | Reprocessing on fix may cause duplicate events |
The secondary risk: engineers often "fix" this by broadening the trust policy to "Principal": "*" or adding sts:AssumeRole with no conditions — which is an IAM privilege escalation vector. Any principal in the account (or cross-account if boundary policies are loose) can then assume the Lambda execution role and inherit its permissions.
How to Fix It (The Solution)
Basic Fix
Open the IAM role attached to your Lambda → Trust relationships tab → Edit trust policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
- "Service": "edgelambda.amazonaws.com"
+ "Service": [
+ "lambda.amazonaws.com",
+ "edgelambda.amazonaws.com"
+ ]
},
"Action": "sts:AssumeRole"
}
]
}
If the role had no Service principal at all:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
- "Principal": {
- "AWS": "arn:aws:iam::123456789012:root"
- },
- "Action": "sts:AssumeRole"
+ "Principal": {
+ "Service": "lambda.amazonaws.com"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "aws:SourceAccount": "123456789012"
+ }
+ }
}
]
}
Enterprise Best Practice
Scope the trust with condition keys to prevent the role from being assumed by Lambda functions in other accounts or by unintended invocation paths. Use aws:SourceArn when the Lambda ARN is known at role creation time:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LambdaAssumeRoleWithESM",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole",
+ "Condition": {
+ "ArnLike": {
+ "aws:SourceArn": "arn:aws:lambda:us-east-1:123456789012:function:your-function-name"
+ },
+ "StringEquals": {
+ "aws:SourceAccount": "123456789012"
+ }
+ }
}
]
}
Why this matters: Without the Condition block, any Lambda function in your account (or a confused deputy attack from a shared service) can assume this role if they can reference it. The aws:SourceArn condition pins the trust to a specific function ARN.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Checkov (IaC scanning — catches this pre-deploy)
Add to your pipeline. Checkov rule CKV_AWS_36 flags Lambda roles missing the correct trust principal:
checkov -d ./terraform --check CKV_AWS_36 --compact
OPA / Conftest Policy (Terraform plan JSON)
package lambda.trust
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_iam_role"
role := resource.change.after
statements := role.assume_role_policy
not contains_lambda_principal(statements)
msg := sprintf("IAM role '%v' is missing lambda.amazonaws.com trust principal", [resource.name])
}
contains_lambda_principal(policy_json) {
policy := json.unmarshal(policy_json)
stmt := policy.Statement[_]
stmt.Principal.Service == "lambda.amazonaws.com"
}
contains_lambda_principal(policy_json) {
policy := json.unmarshal(policy_json)
stmt := policy.Statement[_]
stmt.Principal.Service[_] == "lambda.amazonaws.com"
}
Terraform — Enforce via aws_iam_role data source validation
locals {
lambda_trust_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}]
})
}
resource "aws_iam_role" "lambda_exec" {
name = "lambda-esm-role"
assume_role_policy = local.lambda_trust_policy
}
AWS Config Rule (detective control)
Deploy the managed rule lambda-function-settings-check or a custom Config rule that evaluates GetRole and asserts lambda.amazonaws.com is present in every execution role trust document. Alert to SNS → PagerDuty on NON_COMPLIANT.