Fixing AccessDenied on lambda:InvokeFunction: Why Principal '*' with aws:PrincipalAccount Condition Breaks and How to Resolve It
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Your Lambda resource-based policy uses
Principal: "*"(anonymous/public) with aStringEquals aws:PrincipalAccountcondition. The condition silently evaluates tofalsefor any caller that lacks account context — which includes unauthenticated principals and some AWS service principals — producing a hardAccessDenied. - How to fix it: Replace
Principal: "*"with the explicit IAM principal ARN (account root, role, or service) that needs invoke access. If cross-account scoping is the goal, usePrincipal: {"AWS": "arn:aws:iam::ACCOUNT_ID:root"}directly. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your policy, get corrected JSON without sending your ARNs to a third-party server.
The Incident (What Does the Error Mean?)
You hit this during an Invoke call or an event source mapping test:
{
"Message": "User: arn:aws:sts::123456789012:assumed-role/MyRole/session
is not authorized to perform: lambda:InvokeFunction
on resource: arn:aws:lambda:us-east-1:123456789012:function:MyFunction",
"Code": "AccessDenied"
}
Immediate consequence: Every caller — including same-account roles you explicitly intended to allow — is blocked. The function is effectively unreachable via the resource policy path. If this function sits behind API Gateway or an EventBridge rule, those integrations are silently dead.
The mechanics: When Principal is "*", AWS treats the request as potentially unauthenticated. The condition key aws:PrincipalAccount is not present in the request context for anonymous principals. IAM condition evaluation on a missing key defaults to false. Policy denies. Full stop.
The Attack Vector / Blast Radius
This misconfiguration has a dual failure mode that makes it particularly nasty:
Failure Mode 1 — Availability: Legitimate internal callers are blocked. Any automation, CI pipeline, or service integration that assumed this policy grants access is now broken in production. The error surfaces late because lambda:AddPermission succeeds — the bad policy is accepted without complaint. You only discover the break at runtime.
Failure Mode 2 — Security (the reason you should never use Principal: "*" without understanding the implications): If the condition evaluation logic ever changes, or if you're running in a partition/region where the condition key behavior differs, a Principal: "*" policy with a broken condition becomes an open invoke endpoint for any AWS principal globally — including principals from other AWS accounts. An attacker with any valid AWS credentials and knowledge of your function ARN can invoke it. Lambda functions invoked this way consume concurrency, can exfiltrate environment variable secrets, and can trigger downstream writes to S3, RDS, or SQS.
The condition key aws:PrincipalAccount is not a substitute for a specific Principal. It is a defense-in-depth layer on top of a specific principal, not a replacement for one.
How to Fix It (The Solution)
Basic Fix — Replace the Wildcard Principal
The simplest correct form for same-account access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSameAccountInvoke",
"Effect": "Allow",
- "Principal": "*",
+ "Principal": {
+ "AWS": "arn:aws:iam::123456789012:root"
+ },
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction",
- "Condition": {
- "StringEquals": {
- "aws:PrincipalAccount": "123456789012"
- }
- }
}
]
}
Using the account root ARN as Principal already scopes to your account. The condition is now redundant and removed.
Enterprise Best Practice — Least-Privilege Specific Role + Defense-in-Depth Condition
Never grant account root when you know the exact caller. Scope to the specific role or service, and retain the condition only as a second layer against confused-deputy attacks from AWS services:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificRoleInvoke",
"Effect": "Allow",
- "Principal": "*",
+ "Principal": {
+ "AWS": "arn:aws:iam::123456789012:role/MyInvokerRole"
+ },
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction"
},
{
"Sid": "AllowEventBridgeWithAccountGuard",
"Effect": "Allow",
+ "Principal": {
+ "Service": "events.amazonaws.com"
+ },
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction",
+ "Condition": {
+ "StringEquals": {
+ "aws:SourceAccount": "123456789012"
+ },
+ "ArnLike": {
+ "aws:SourceArn": "arn:aws:events:us-east-1:123456789012:rule/MyRule"
+ }
+ }
}
]
}
Key distinctions:
- For IAM principals (roles, users): use
aws:PrincipalAccountas a condition only whenPrincipalis already a specific ARN — never with"*". - For AWS service principals (EventBridge, S3, SNS): use
aws:SourceAccountandaws:SourceArnconditions.aws:PrincipalAccountis not populated for service principals. - Never use
Principal: "*"on a Lambda resource policy unless you are intentionally building a public function (rare, and should be combined withaws:SourceVpcor function URL auth controls).
💡 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
This class of misconfiguration is entirely preventable at the PR stage.
1. Checkov — block wildcard Lambda principals at plan time:
# .checkov.yaml
checks:
- CKV_AWS_45 # Lambda function not publicly accessible
- CKV_AWS_272 # Lambda resource-based policy no wildcard principal
Run in your pipeline:
checkov -d ./terraform --check CKV_AWS_45,CKV_AWS_272 --hard-fail-on HIGH
2. OPA/Conftest policy for raw CloudFormation or Terraform JSON:
package lambda.resource_policy
deny[msg] {
stmt := input.Statement[_]
stmt.Principal == "*"
not stmt.Condition.StringEquals["aws:SourceVpc"]
msg := sprintf(
"Lambda statement '%v' uses Principal '*' without vpc-scoped condition. Specify an explicit IAM or service principal.",
[stmt.Sid]
)
}
3. AWS Config Rule: Enable lambda-function-public-access-prohibited as a managed Config rule. It flags any resource-based policy that resolves to public access after condition evaluation.
4. Terraform aws_lambda_permission — always use principal explicitly:
# Never do this:
# principal = "*"
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.my_function.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.my_rule.arn
source_account = data.aws_caller_identity.current.account_id
}
The source_account argument in aws_lambda_permission maps directly to the aws:SourceAccount condition — Terraform handles the condition injection for you when using the resource type correctly.