FR
live

Lambda gains full IAM resource policies, and one API call now wipes every trigger

On August 25, 2026, AWS opened full IAM resource-based policies to Lambda functions: a single JSON document, the complete range of condition keys, and explicit Deny statements. Platform teams gain precision, but PutResourcePolicy overwrites the whole policy in one call — adopt it with a read-modify-write and an audit of existing triggers first.

A grid of identical dark archive drawers, one single drawer pulled half-open and glowing amber.

August 25, 2026. AWS opened full IAM resource-based policies to Lambda functions, in every commercial Region and at no extra cost. Three new actions — PutResourcePolicy, GetResourcePolicy, and DeleteResourcePolicy — arrive on a fresh API path, /2026-07-09/resource-policy/, while the legacy AddPermission path stays on /2015-03-31/. The promise is real: a complete JSON document, the full range of IAM condition keys, and explicit Deny statements. But the announcement buries the detail that changes the security picture: a single PutResourcePolicy call overwrites the entire policy, including every trigger.

What actually changed

Since 2015, a Lambda function’s resource policy could only be built one statement at a time through AddPermission. That path accepted just three condition keysaws:SourceArn, aws:SourceAccount, and aws:PrincipalOrgID — and allowed no explicit Deny. You could not restrict by source IP or principal tag, nor grant several services in a single statement. For a team managing multi-account functions, every new service allowed to invoke a function meant one more API call, scattered across accounts.

The new path accepts a complete JSON document. You can now write Deny statements, use the full set of global IAM condition keys, and grant multiple services access in one statement instead of several. This is a catch-up: S3 buckets, SQS queues, and SNS topics have had full resource policies for years. Lambda was the holdout, shackled by its 2015 heritage. The docs are unambiguous about the direction: AWS recommends defining complete JSON policies to add resource-based permissions.

The trap: PutResourcePolicy replaces everything

The operational hazard is spelled out in the developer guide: put-resource-policy replaces any existing policy, and if the function already has permissions added through add-permission, it overwrites them. Almost nobody writes trigger permissions by hand. Every AWS::Lambda::Permission in a CloudFormation stack, every event source wired through the console, every S3 notification or EventBridge rule target adds a statement through AddPermission. One PutResourcePolicy from a platform team, aimed at a function whose triggers were provisioned by three different stacks, removes all of them in a single call. The function’s code and configuration do not change — the triggers simply stop being authorized.

The reverse direction behaves differently: add-permission called after put-resource-policy appends a statement to the existing document rather than overwriting it. So a CloudFormation stack that runs after your JSON rollout repairs its own statement, and one that ran before it does not. The drift shows up on the next deploy, not at the moment of damage.

Three permissions for one API

The other surprise is arithmetic. To use the operation AWS recommends for least-privilege management, an identity must hold three IAM permissions: lambda:PutResourcePolicy, lambda:AddPermission and lambda:RemovePermission. A role holding only lambda:PutResourcePolicy gets AccessDenied. A role granted all three can also call lambda:AddPermission and lambda:RemovePermission directly, outside the JSON editor, on any function its Resource block reaches.

The result: the identity-side blast radius of a “policy admin” role goes up, even though the policy it writes can now be far more precise. A team that gates IAM changes on a permission diff will see that diff get worse the day it adopts the feature.

The RevisionId documentation conflict

PutResourcePolicy takes an optional RevisionId. If it does not match, the call fails with PreconditionFailedException (HTTP 412), stopping two concurrent writers from clobbering each other. The question is where that value comes from — and AWS answers it twice, differently, on the same page.

The API error block splits the world into two cases: AddPermission and RemovePermission should call GetPolicy; “all other API operations” should call GetFunction or GetAlias. PutResourcePolicy falls into the second bucket on a literal reading. Except GetFunction returns the function configuration’s revision ID — which tracks code and configuration changes — not the policy revision that GetResourcePolicy returns. An engineer reading that block mid-incident, after a 412, follows the instructions, passes a revision ID from the wrong object, and the retry loop never converges.

The rule to encode in tooling is simple: policy revision IDs come from GetResourcePolicy (new path) or GetPolicy (legacy path). GetFunction is never the right source for a policy precondition.

The 20 KB ceiling and the control-plane budget

Two limits did not move. First, the policy ceiling stays at 20 KB (20,480 characters exactly), the value inherited from the legacy path. A resource policy is not a full IAM inventory — it stays short by nature — but teams that pack dozens of trigger statements into one will eventually hit the wall.

The second is more insidious. Lambda’s control-plane quotas have not changed: GetFunction at 100 requests per second, GetPolicy on a dedicated 15 rps, and everything else sharing one 15 rps bucket that cannot be raised. GetResourcePolicy and PutResourcePolicy land in that shared bucket. The read-modify-write AWS recommends — GetResourcePolicy then PutResourcePolicy — draws 100 % more of the shared budget than the legacy GetPolicy then AddPermission pair, and roughly halves effective throughput (about 7.5 functions per second versus 15).

The safe sequence, in practice:

bash
# 1. Read the current policy AND its RevisionId (never via GetFunction)
REVISION_ID=$(aws lambda get-resource-policy \
  --function-name payments-handler \
  --query RevisionId --output text)

# 2. Merge, then rewrite with the freshly read RevisionId
aws lambda put-resource-policy \
  --function-name payments-handler \
  --policy file://policy-merged.json \
  --revision-id "$REVISION_ID"

One note: the announcement is scoped to commercial Regions. The ResourceArn regexes on all three new actions do accept gov, iso, and eusc partition ARNs, but a pattern that matches a string is not confirmation that the operation is enabled in that partition. Test in your own partition before planning a migration.

Migrating without breaking things: a phased strategy

The right rollout is gradual, never a fleet-wide cutover. Start with a pilot function that has no sensitive triggers, write its full JSON policy, and verify against a diff that the existing statements are carried over. Only move the rest of the fleet once the read-modify-write is bedded in and RevisionId handling is correct.

On the infrastructure-as-code side, the cutover is a double trap. CloudFormation, SAM, and Terraform support the new path, but a stack that still creates AWS::Lambda::Permission resources will re-write its statements after your PutResourcePolicy — or lose them, depending on execution order. Consistency requires migrating the stack and the policy together, in the same deploy, otherwise drift sets in silently.

Finally, measure the control-plane cost before generalizing: if your pipeline reconciles hundreds of functions, the GetResourcePolicy + PutResourcePolicy pair at 7.5 functions per second can throttle your deployments. Sequence the migration in batches, or keep the legacy path for high-volume trigger functions.

Verdict

If you are a platform team managing multi-account functions, adopt full policies — they finally remove the three-condition-key limit and enable real least privilege by IP or by tag. But protect yourself: always read the RevisionId through GetResourcePolicy, merge before writing, and audit existing AWS::Lambda::Permission resources and triggers first so the first call does not wipe them. If you run a handful of functions with simple triggers, the legacy AddPermission remains perfectly adequate and avoids any overwrite risk.

References

The cyber brief, every Tuesday

The flaws that matter and the patches to apply, in a ten-minute read.

No spam. One-click unsubscribe.
read next

On the same topic

The C8gn Graviton4 instances at 600 Gbps reach Paris and now span 30 AWS regions

On August 28 2026 AWS made EC2 C8gn instances, powered by Graviton4 and capable of 600 Gbps of network bandwidth, available in the Europe (Paris) region. For teams running virtual network appliances or high-throughput analytics, this European expansion reshapes the cost-performance case for arm64.

← Back to the feed

Type at least two characters.

navigate open esc dismiss