Skip to content

( aws-s3: ): addEventNotification() removal fails with AccessDenied on multi-bucket stacks #37667

Closed

Description

@abidhasan-aws

Describe the bug

Removing an addEventNotification() call from an S3 bucket and redeploying causes the stack update to fail with AccessDenied on s3:PutBucketNotification. The BucketNotificationsHandler custom resource Lambda loses its IAM permission before CloudFormation invokes it to clean up the notification configuration.

This is a regression introduced in aws-cdk-lib v2.215.0 by PR #35334 (commit c0300d266a), which scoped the handler's IAM policy from Resource: "*" to per-bucket ARNs.

Trigger condition: the bug requires the stack to contain two or more buckets with notifications, where at least one is removed while at least one other remains. In that case CloudFormation treats the IAM policy change as an update (revoking the removed bucket's statement mid-deploy, before the custom resource delete runs). A stack with only a single bucket+notification is unaffected because CloudFormation treats it as a delete of the whole policy instead, which preserves the ordering naturally.

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Library Version

v2.214.0

Expected Behavior

Removing an addEventNotification() call and redeploying should cleanly delete the Custom::S3BucketNotifications resource without errors. This worked correctly in v2.214.0 and earlier.

Current Behavior

The stack update fails with:

Received response status [FAILED] from custom resource. Message returned:
Error: An error occurred (AccessDenied) when calling the PutBucketNotificationConfiguration operation:
User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/BucketNotificationRemoval-BucketNotificationsHandle-vr5VUtFZArg8/BucketNotificationRemoval-BucketNotificationsHandl-EMKp3yKgUwxS
is not authorized to perform: s3:PutBucketNotification on resource: "arn:aws:s3:::bucketName"
because no identity-based policy allows the s3:PutBucketNotification action.
See the details in CloudWatch Log Stream: 2026/04/16/[$LATEST]ecea16b4dc70413a885d99b63fb1a2b1
(RequestId: b40d861e-6120-4554-984e-8d1bee50f0ac)

Reproduction Steps

  1. Create a stack with two S3 buckets, each with an event notification targeting a Lambda function:
import * as cdk from 'aws-cdk-lib/core';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import { Construct } from 'constructs';

export class BucketNotificationRemovalTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket1 = new s3.Bucket(this, 'Bucket1', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
    const bucket2 = new s3.Bucket(this, 'Bucket2', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
    const fn = new lambda.Function(this, 'MyLambda', {
      runtime: lambda.Runtime.PYTHON_3_13,
      handler: 'lambda_function.lambda_handler',
      code: lambda.Code.fromInline('foo'),
    });

    bucket1.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(fn));
    bucket2.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(fn));
  }
}
  1. Deploy the stack: cdk deploy
  2. Comment out or delete the bucket2.addEventNotification(...) line
  3. Deploy again: cdk deploy
  4. Stack update fails with the AccessDenied error above

Possible Solution

Root Cause

PR #35334 (commit c0300d266a, released in v2.215.0) changed the BucketNotificationsHandler singleton's IAM policy from a single wildcard statement to per-bucket ARN-scoped statements. This was a security improvement (closing #35331 and #5925), but it introduced a CloudFormation resource-ordering race condition on removal:

  1. CloudFormation updates the handler role's IAM policy, dropping the s3:PutBucketNotification statement for the removed bucket's ARN.
  2. CloudFormation deletes the Custom::S3BucketNotifications resource for that bucket, invoking the handler Lambda.
  3. The handler calls PutBucketNotificationConfiguration to clear the notification config, but the permission was already revoked in step 1.

There is no DependsOn between the IAM policy update and the custom resource, so CloudFormation is free to process step 1 before step 2. Pre-v2.215.0, the wildcard Resource: "*" statement was shared across all buckets and was never partially removed, so the handler always retained permission.

Additional Information/Context

Verification Matrix

Version Initial deploy Remove one notification
v2.214.0 ✅ SUCCESS ✅ SUCCESS
v2.215.0 ✅ SUCCESS ❌ FAIL — AccessDenied
v2.250.0 ✅ SUCCESS ❌ FAIL — AccessDenied

v2.214.0 is the last unaffected version. Every release from v2.215.0 through latest exhibits the bug.

AWS CDK Library version (aws-cdk-lib)

v2.250.0

AWS CDK CLI version

2.1118.4

Node.js Version

22

OS

MAC

Language

TypeScript

Language Version

No response

Other information

No response

Activity

  1. added
    bugThis issue is a bug.
    needs-triageThis issue or PR still needs to be triaged.
    on Apr 22, 2026
  2. abidhasan-aws commented on Apr 22, 2026

    @abidhasan-aws
    ContributorAuthor

    Adding possible workaround here until a permanent fix is prepared.

    WORKAROUND 1

    If you can downgrade to aws-cdk-lib v2.214.0, that should solve the problem — it is the last working version for this scenario.

    WORKAROUND 2 : Escape hatch to restore wildcard permission

    ⚠️ This grants the singleton BucketNotificationsHandler Lambda s3:PutBucketNotification on *, which is broader than CDK's default since v2.215.0. Step 3 removes it once you are done.

    This keeps you on your current CDK version.
    It is a 3-step process:

    1. Add the escape hatch
    2. Remove the notification
    3. Remove the escape hatch.

    The escape hatch attaches a standalone IAM Policy (s3:PutBucketNotification on *) that CloudFormation does not touch during the removal, so the handler Lambda keeps its permission. This uses the CDK escape hatch pattern.

    Step 1. Add the escape hatch and deploy.

    Using the reproduction stack as the starting point, add the escape hatch at the end of the stack.

      import * as cdk from 'aws-cdk-lib/core';
      import * as lambda from 'aws-cdk-lib/aws-lambda';
      import * as s3 from 'aws-cdk-lib/aws-s3';
      import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
      import * as iam from 'aws-cdk-lib/aws-iam';                          // NEW
      import { Construct } from 'constructs';
    
      export class BucketNotificationRemovalTestStack extends cdk.Stack {
        constructor(scope: Construct, id: string, props?: cdk.StackProps) {
          super(scope, id, props);
    
          const newBucket1 = new s3.Bucket(this, '<somebucketname>1', {
            bucketName: '<somebucketname>1',
            removalPolicy: cdk.RemovalPolicy.DESTROY,
          });
          const newBucket2 = new s3.Bucket(this, '<somebucketname>2', {
            bucketName: '<somebucketname>2',
            removalPolicy: cdk.RemovalPolicy.DESTROY,
          });
          const lambdaDestinationFunction = new lambda.Function(this, 'MyLambda', {
            runtime: lambda.Runtime.PYTHON_3_13,
            handler: 'lambda_function.lambda_handler',
            code: lambda.Code.fromInline('foo'),
          });
    
          newBucket1.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(lambdaDestinationFunction));
          newBucket2.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(lambdaDestinationFunction));
    
          // --- ESCAPE HATCH START ---
          const handler = cdk.Stack.of(this)
            .node.tryFindChild('BucketNotificationsHandler050a0587b7544547bf325f094a3db834') as lambda.Function;
    
          if (!handler) {
            throw new Error('BucketNotificationsHandler singleton not found — ensure at least one addEventNotification() is declared before the escape hatch.');
          }
    
          handler.role!.attachInlinePolicy(new iam.Policy(this, 'HandlerWildcardFallback', {
            statements: [new iam.PolicyStatement({
              actions: ['s3:PutBucketNotification'],
              resources: ['*'],
            })],
          }));
          // --- ESCAPE HATCH END ---
        }
      }
    

    Notice that both addEventNotification() calls are still present. Do not remove anything yet, this step only installs the fallback policy.

    Optional sanity check: confirm the policy appears in the synthesized template:

      cdk synth | grep -A 5 HandlerWildcardFallback
    

    Deploy:

      cdk deploy
    

    Step 2. Remove the notification and deploy.

    Delete (or comment out) the addEventNotification() call for the bucket you no longer want monitored. For example, remove the line for newBucket2:

      newBucket1.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(lambdaDestinationFunction));
      // newBucket2.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(lambdaDestinationFunction));   // REMOVED
    

    Deploy:

      cdk deploy
    

    The per-bucket IAM statement for newBucket2 is removed from the template, but the HandlerWildcardFallback policy remains, so the handler Lambda retains permission and the cleanup succeeds without AccessDenied.

    Step 3. Remove the escape hatch and deploy (Strongly recommended).

    Once Step 2 has succeeded, delete the entire escape hatch block (everything between // --- ESCAPE HATCH START --- and // --- ESCAPE HATCH END ---) and also remove the import * as iam line if it is no longer used.

    Deploy:

      cdk deploy
    

    This returns you to a clean state with the notification removed from the stack.

    Hope this helps anyone else who hits this. Happy to clarify anything.

  3. pahud commented on Apr 23, 2026

    @pahud
    Contributor

    Hi there,

    I was able to reproduce this locally on aws-cdk-lib v2.250.0 (latest) by synthesizing both the "before" (two buckets with notifications) and "after" (one notification removed) templates. I also cross-verified against v2.214.0, which uses a wildcard Resource: "*" and is unaffected.

    From my view, the root cause is in addHandlerPermissions() — the per-bucket ARN scoping introduced by PR #35334 is a valid security improvement, but it created a CloudFormation ordering gap. When a notification is removed, CloudFormation is free to update the shared IAM policy (revoking the bucket's ARN) before invoking the custom resource handler to clean up.

    Update: My initial thought was to add a DependsOn from each Custom::S3BucketNotifications to the handler's shared RoleDefaultPolicy. However, after live CloudFormation testing, I confirmed this does not work — DependsOn only controls create/delete ordering, not update ordering. Since the shared policy is updated (not deleted) when one of multiple buckets is removed, CloudFormation ignores the DependsOn and updates the policy first.

    The fix that works is using a dedicated IAM policy per bucket notification instead of the shared DefaultPolicy. When a notification is removed, its dedicated policy is deleted (not updated), and DependsOn correctly ensures the custom resource is deleted before its policy.

    PR: #37678

  4. added and removed
    needs-triageThis issue or PR still needs to be triaged.
    on Apr 23, 2026
  5. added 2 commits that reference this issue on Apr 23, 2026
    87a6d7a
    3e71cd4
  6. added a commit that references this issue on Apr 28, 2026
    dc5be98
  7. github-actions commented on Apr 28, 2026

    @github-actions
    Contributor

    Comments on closed issues and PRs are hard for our team to see.
    If you need help, please open a new issue that references this one.

  8. locked as resolved and limited conversation to collaborators on Apr 28, 2026
  9. added a commit that references this issue on Jul 29, 2026
    fd85f8a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/aws-s3Related to Amazon S3bugThis issue is a bug.p0potential-regressionMarking this issue as a potential regression to be checked by team member

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions