AWS Lambda Now Supports Full IAM Resource-Based Policies, So I Tried It Out Cross-Account

AWS Lambda Now Supports Full IAM Resource-Based Policies, So I Tried It Out Cross-Account

AWS Lambda now supports full IAM resource-based policies via PutResourcePolicy. I tested how a single JSON policy behaves with account-wide Allow plus an explicit Deny for a role, negated-condition Denies, and cross-account management actions.
2026.09.01

This page has been translated by machine translation. View original

Introduction

On August 25, 2026, it was announced that AWS Lambda now supports full IAM resource-based policies. With PutResourcePolicy, you can set a function's resource-based policy as a complete JSON document.

https://aws.amazon.com/about-aws/whats-new/2026/08/aws-lambda-full-iam-resource-based-policies/

What's New and the Lambda Developer Guide explain that IAM condition keys can be used in resource-based policies.

The main differences between AddPermission and PutResourcePolicy are the unit of configuration, how conditions are specified, and whether an explicit Deny is possible.

Aspect AddPermission PutResourcePolicy
Unit of configuration 1 statement per call The entire JSON policy
Specifying conditions Dedicated parameters only Condition in JSON
Explicit Deny Cannot be written Can be written

In the SYNOPSIS of aws lambda add-permission, the following 3 lines can be used to narrow down callers by condition (in the original, other parameters are interspersed between these 3 lines).

          [--source-arn <value>]
          [--source-account <value>]
          [--principal-org-id <value>]

Other than these 3 lines, there is no parameter for specifying an arbitrary IAM condition key.

In this article, I check the behavior when an account-level allow and an explicit Deny for a specific role are combined into a single JSON policy. I also verify a Deny using negative conditions, cross-account delegation of management actions, and how the policy changes when PutResourcePolicy and AddPermission are executed in succession.

What I Tested

I tested full IAM resource-based policies in a setup where a function is operated from an IAM role in a different account.

Test Environment

  • AWS CLI: 2.36.34 (the put-resource-policy subcommand is available from 2.36.28 onward)
  • Region: ap-northeast-1
  • Function: rbp-demo (runtime: python3.13, deployed in account 111122223333)
  • Caller: IAM role cross-account-invoker in account 444455556666

For cross-account invocation, permission is required in both the resource-based policy and the identity-based policy. AdministratorAccess is attached to the caller role this time, so the identity-based policy requirement is satisfied.

Combining Allow and Explicit Deny

First, I applied a policy that did not include a Deny statement. I specified the ARN of the caller role in Principal, allowed lambda:InvokeFunction, and ran Invoke.

{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

The function's return value was {"message": "invoked", "event": {"from": "caller"}}.

Next, I combined an account-level Allow and a Deny for a specific role into a single JSON policy and applied it with put-resource-policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "allow-account",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"
    },
    {
      "Sid": "deny-specific-role",
      "Effect": "Deny",
      "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo",
      "Condition": {
        "ArnEquals": {
          "aws:PrincipalArn": "arn:aws:iam::444455556666:role/cross-account-invoker"
        }
      }
    }
  ]
}

Because the Deny statement also has a Condition, the denial applies only to principals whose aws:PrincipalArn matches. The only principal used for invocation this time is the role that matches this condition.

An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:sts::444455556666:assumed-role/cross-account-invoker/botocore-session-xxxxxxxxxx is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo with an explicit deny in a resource-based policy

The trailing with an explicit deny in a resource-based policy shows that the cause is an explicit Deny.

AddPermission requires --principal, and this time I passed an account ID. With PutResourcePolicy, on the other hand, you can also write a fully public specification with no conditions in the JSON.

I applied policies with "*" as the Principal in 3 patterns. In this test environment, a RevisionId was returned for all of them.

  • With only aws:SourceIp as the condition: accepted
  • With only aws:PrincipalOrgID as the condition: accepted
  • With no conditions at all: accepted

Delegating Management Actions

Permission to create aliases could also be delegated to another account using a resource-based policy. For the ARN of the caller role, I allowed three actions—function invocation, alias creation, and configuration retrieval—in a single statement.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "allow-invoke-and-alias-mgmt",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::444455556666:role/cross-account-invoker" },
      "Action": ["lambda:InvokeFunction", "lambda:CreateAlias", "lambda:GetFunctionConfiguration"],
      "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"
    }
  ]
}

I applied this policy and ran create-alias from the caller account 444455556666.

{
    "AliasArn": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo:prod-from-caller",
    "Name": "prod-from-caller",
    "FunctionVersion": "1",
    "Description": "",
    "RevisionId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
}

The alias was created in account 111122223333, where the function is deployed, and was linked to version 1.

Denying Everyone Except the Specified Principal

If you write the Deny's Condition in negated form, you can deny everyone other than the specified principal in one go. This is a style commonly used in S3 bucket policies.

For comparison, I first applied only the account-level Allow and ran Invoke from two roles in the caller account; both returned status code 200.

Next, I kept the same Allow and added a Deny with ArnNotEquals.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "allow-account",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"
    },
    {
      "Sid": "deny-unless-allowed-role",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo",
      "Condition": {
        "ArnNotEquals": {
          "aws:PrincipalArn": "arn:aws:iam::444455556666:role/cross-account-invoker"
        }
      }
    }
  ]
}

These are the results of invoking from 3 principals with this policy.

Caller Deny condition met Result
cross-account-invoker in 444455556666 No 200
other-invoker in 444455556666 Yes AccessDeniedException
owner-admin in 111122223333 Yes AccessDeniedException

The third row is a role in the account that owns the function. AdministratorAccess is attached, but it was denied.

An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:sts::111122223333:assumed-role/owner-admin/botocore-session-xxxxxxxxxx is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo with an explicit deny in a resource-based policy

Within the same account, Lambda can be invoked with an identity-based policy alone. Without this Deny, the invocation from owner-admin would have gone through. Because AddPermission cannot add a Deny statement, this form can only be configured with PutResourcePolicy.

Note that this Deny limits Action to lambda:InvokeFunction. Even with the Deny in effect, get-resource-policy and get-function-configuration from the owning account succeeded. The permission to replace the policy remains.

The same form can also be written for source IPs. It is a Deny that drops the principal condition and uses only NotIpAddress as the condition.

{
  "Sid": "deny-unless-from-allowed-ip",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo",
  "Condition": {
    "NotIpAddress": { "aws:SourceIp": "198.51.100.10/32" }
  }
}

During testing, specifying the actual source IP with /32 returned 200, and changing the allowed CIDR to 203.0.113.0/24 resulted in a denial. In the JSON shown here, the source IP has been replaced with an example value. As with aws:PrincipalArn, owner-admin in the owning account was also denied when its source IP fell outside the range.

I have not tested it this time, but you can also treat VPC endpoints and AWS services as exceptions. For access via a VPC endpoint, use aws:SourceVpce or aws:SourceVpc. For invocations from AWS services, distinguish them with aws:PrincipalIsAWSService. For details, see the IAM global condition keys reference.

https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html

Running PutResourcePolicy and AddPermission in Succession

I ran add-permission and put-resource-policy in succession and checked the resource-based policy at each stage. The following is the policy body included in the get-resource-policy response.

First, I ran add-permission --statement-id via-add --principal 444455556666.

{"Version":"2012-10-17","Id":"default","Statement":[{"Sid":"via-add","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::444455556666:root"},"Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"}]}

When an account ID is passed to --principal, the policy's Principal was expanded to the root ARN.

Next, I passed JSON containing only via-put to put-resource-policy.

{"Version":"2012-10-17","Statement":[{"Sid":"via-put","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::444455556666:role/cross-account-invoker"},"Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"}]}

This policy does not include via-add.

Then I ran add-permission --statement-id via-add-2 --principal s3.amazonaws.com.

{"Version":"2012-10-17","Statement":[{"Sid":"via-put","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::444455556666:role/cross-account-invoker"},"Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"},{"Sid":"via-add-2","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":"lambda:InvokeFunction","Resource":"arn:aws:lambda:ap-northeast-1:111122223333:function:rbp-demo"}]}

With via-put remaining, via-add-2 was added.

Summary

With PutResourcePolicy, I was able to apply an account-level allow and an explicit Deny for a specific role together in a single JSON policy. Not only Invoke but also management actions such as alias creation could be delegated across accounts.

PutResourcePolicy lets you set a Lambda function's entire resource-based policy in JSON, and write arbitrary Conditions and explicit Denies. In particular, by using negative conditions such as ArnNotEquals and NotIpAddress, you can make only specific roles or source IPs exceptions and deny all other invocations in one go.

This Deny also applies to the account that owns the function, and can deny same-account invocations that were permitted by identity-based policies. Access control close to S3 bucket policies is now possible for Lambda functions too. If you felt constrained by the resource-based policies of Lambda functions until now, please give this update a try.

Share this article

AWSのお困り事はクラスメソッドへ