Initializing Enclave...

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 a StringEquals aws:PrincipalAccount condition. The condition silently evaluates to false for any caller that lacks account context — which includes unauthenticated principals and some AWS service principals — producing a hard AccessDenied.
  • 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, use Principal: {"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:PrincipalAccount as a condition only when Principal is already a specific ARN — never with "*".
  • For AWS service principals (EventBridge, S3, SNS): use aws:SourceAccount and aws:SourceArn conditions. aws:PrincipalAccount is 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 with aws:SourceVpc or 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.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →