Repository navigation
( aws-s3: ): addEventNotification() removal fails with AccessDenied on multi-bucket stacks #37667
Description
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
- 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));
}
}- Deploy the stack:
cdk deploy - Comment out or delete the
bucket2.addEventNotification(...)line - Deploy again:
cdk deploy - 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:
- CloudFormation updates the handler role's IAM policy, dropping the
s3:PutBucketNotificationstatement for the removed bucket's ARN. - CloudFormation deletes the
Custom::S3BucketNotificationsresource for that bucket, invoking the handler Lambda. - The handler calls
PutBucketNotificationConfigurationto 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
abidhasan-aws commentedon Apr 22, 2026
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
BucketNotificationsHandlerLambdas3:PutBucketNotificationon*, 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:
The escape hatch attaches a standalone IAM Policy (
s3:PutBucketNotificationon*) 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.
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:
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 fornewBucket2:Deploy:
The per-bucket IAM statement for
newBucket2is removed from the template, but theHandlerWildcardFallbackpolicy 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 theimport * as iamline if it is no longer used.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.
pahud commentedon Apr 23, 2026
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
DependsOnfrom eachCustom::S3BucketNotificationsto the handler's sharedRoleDefaultPolicy. However, after live CloudFormation testing, I confirmed this does not work —DependsOnonly controls create/delete ordering, not update ordering. Since the shared policy is updated (not deleted) when one of multiple buckets is removed, CloudFormation ignores theDependsOnand 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), andDependsOncorrectly ensures the custom resource is deleted before its policy.PR: #37678
fix(s3): use per-bucket IAM policies for BucketNotificationsHandler
fix(s3): use per-bucket IAM policies for BucketNotificationsHandler
fix(s3): resolve S3 notification removal race condition (#37708)
github-actions commentedon Apr 28, 2026
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.
fix(s3): resolve S3 notification removal race condition (aws#37708)