Skip to content

Conversation

@pahud
Copy link
Contributor

@pahud pahud commented Aug 26, 2025

Issue # (if applicable)

Closes #35331 #5925

Reason for this change

The S3 BucketNotificationsHandler Lambda function created by CDK for S3 bucket notifications receives overly broad IAM permissions with "Resource": "*" instead of being scoped to specific bucket ARNs. This violates the principle of least privilege and creates a security risk where the handler could potentially modify notifications for any S3 bucket in the account, preventing security teams from approving deployments.

Description of changes

Implemented scoped IAM permissions for the S3 BucketNotificationsHandler by leveraging the existing grantBucketNotifications() method and the @aws-cdk/aws-iam:minimizePolicies feature flag:

  • Simplified approach: Removed complex Aspect-based implementation in favor of using existing CDK mechanisms
  • Feature flag utilization: The @aws-cdk/aws-iam:minimizePolicies feature flag automatically consolidates IAM policies with specific bucket ARNs
  • Grant-based permissions: Uses the existing grantBucketNotifications() method which already provides scoped permissions to specific bucket ARNs
  • Automatic consolidation: When multiple buckets use the same handler, policies are automatically consolidated into a single statement with all bucket ARNs
  • Backward compatibility: Maintains singleton pattern and all existing functionality - no breaking changes

Before: IAM policy contained "Resource": "*" (wildcard permissions)
After: IAM policy contains "Resource": [{"Fn::GetAtt": ["BucketName", "Arn"]}, ...] (scoped permissions)

Describe any new or updated permissions being added

Security improvement: This change reduces permissions rather than adding new ones. The handler now receives more restrictive IAM permissions scoped only to the specific S3 buckets it needs to manage, implementing the principle of least privilege. No new permissions are required.

Description of how you validated changes

  • Unit tests: All existing 22 notification tests pass successfully (100% success rate). Tests already expect scoped permissions due to the existing grant method implementation
  • Integration tests: Existing integration test snapshots show the correct IAM policy structure with specific bucket ARNs instead of wildcards. All integration tests pass
  • CloudFormation validation: Verified that generated templates contain specific bucket ARNs instead of wildcards and deploy successfully
  • Regression testing: Confirmed all existing S3 bucket notification functionality works unchanged, including EventBridge notifications, filter rules, and both managed and unmanaged bucket scenarios

Potential Concerns

  1. Simplified Implementation: The current approach leverages existing CDK mechanisms (grantBucketNotifications() method and @aws-cdk/aws-iam:minimizePolicies feature flag) rather than implementing custom Aspect logic. This is actually a strength as it reduces complexity and maintenance burden while achieving the same security outcome.

  2. Feature Flag Dependency: The solution relies on the @aws-cdk/aws-iam:minimizePolicies feature flag for policy consolidation. This flag is already widely adopted and is part of CDK's recommended feature flags, making this a low-risk dependency.

  3. Potential for Regressions: The PR fixes a security vulnerability that has been present for a while. The existing comprehensive test suite provides good coverage, and the integration tests serve as the best defense against future regressions.

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@aws-cdk-automation aws-cdk-automation requested a review from a team August 26, 2025 19:53
@github-actions github-actions bot added bug This issue is a bug. effort/medium Medium work item – several days of effort p1 labels Aug 26, 2025
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Aug 26, 2025
@pahud pahud changed the title fix(aws-s3): scope BucketNotificationsHandler IAM permissions to specific bucket ARNs fix(s3): scope BucketNotificationsHandler IAM permissions to specific bucket ARNs Aug 26, 2025
@pahud pahud marked this pull request as ready for review August 26, 2025 20:06
@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Aug 27, 2025
@Abogical Abogical self-assigned this Aug 27, 2025
Copy link
Member

@Abogical Abogical left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using CDK aspects for this is overkill, can't we just grant the resource directly with the s3 bucket? preferably using grant methods instead of inline policies?

@aws-cdk-automation aws-cdk-automation removed the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Aug 27, 2025
@pahud
Copy link
Contributor Author

pahud commented Aug 27, 2025

@Abogical

We actually have @aws-cdk/aws-iam:minimizePolicies which defaults "true". I will investigate if it already works and report here.

@Abogical Abogical removed their assignment Aug 27, 2025
@ozelalisen ozelalisen self-assigned this Aug 28, 2025
Copy link
Member

@ozelalisen ozelalisen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left comment

Comment on lines 110 to 138

/**
* Grant permissions for managing bucket notifications.
*
* Grants scoped IAM permissions to the specific bucket ARN instead of using wildcard permissions.
* This implements the principle of least privilege by limiting the handler's access to only
* the buckets it needs to manage.
*
* @param bucketArn The ARN of the bucket to grant permissions for
* @param isUnmanaged Whether this is an unmanaged (imported) bucket that needs GetBucketNotification permissions
*/
public grantBucketNotifications(bucketArn: string, isUnmanaged: boolean = false) {
// All buckets need PutBucketNotification to set/update notification configurations
this.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:PutBucketNotification'],
resources: [bucketArn],
}));

// Unmanaged (imported) buckets need GetBucketNotification to read existing configurations
// before merging with new notifications. Managed buckets don't need this since CDK
// controls their complete notification state from creation.
if (isUnmanaged) {
this.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:GetBucketNotification'],
resources: [bucketArn],
}));
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grantBucketNotifications() method should be private within the notifications-resource.ts as it is not needed to have this method as public. It is also not used inside resource handler. Let's move method and make it private.

Copy link
Contributor Author

@pahud pahud Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ozelalisen

The grantBucketNotifications() method needs to remain public as it's actively used by the BucketNotifications class and follows established CDK design patterns. Making it private would break existing functionality and violate CDK conventions.

Current Usage Evidence

The method is called in packages/aws-cdk-lib/aws-s3/lib/notifications-resource/notifications-resource.ts at line 142:

private createResourceOnce() {
  if (!this.resource) {
    const handler = NotificationsResourceHandler.singleton(this, {
      role: this.handlerRole,
    });

    let managed = this.bucket instanceof Bucket;

    // Feature flag handling for imported buckets
    if (cdk.FeatureFlags.of(this).isEnabled(cxapi.S3_KEEP_NOTIFICATION_IN_IMPORTED_BUCKET)) {
      managed = false;
    }

    // ACTIVE USAGE - External class calling public method
    handler.grantBucketNotifications(this.bucket.bucketArn, !managed);

    this.resource = new cdk.CfnResource(this, 'Resource', {
      type: 'Custom::S3BucketNotifications',
      // ... properties
    });
  }
  return this.resource;
}

Design Pattern Analysis

Current Approach (Public Instance Method)

// Object-oriented, follows CDK patterns
handler.grantBucketNotifications(bucketArn, isUnmanaged);

Advantages:

  • Follows established CDK patterns (bucket.grantRead(), table.grantWriteData(), etc.)
  • Encapsulates permission management within the handler construct
  • Object-oriented design - handler manages its own IAM state
  • Consistent with CDK ecosystem conventions
  • Future-proof for adding permission tracking or validation logic
  • Clear ownership - the handler is responsible for its own permissions

Alternative Approach (Private/Static Function)

// Procedural approach, would require refactoring
NotificationsResourceHandler.grantBucketNotifications(handler, bucketArn, isUnmanaged);
// or
grantBucketNotifications(handler, bucketArn, isUnmanaged);

Disadvantages:

  • Not following our established CDK design conventions
  • Exposes internal role management to external callers
  • Inconsistent with other grant* methods across the CDK ecosystem
  • Harder to extend with additional logic or state management

References

We currently have some public grant* method pattern is used throughout CDK:

// S3 Bucket
bucket.grantRead(role);
bucket.grantWrite(role);

// DynamoDB Table  
table.grantReadData(role);
table.grantWriteData(role);

// Lambda Function
func.grantInvoke(role);

// Our Handler (consistent pattern)
handler.grantBucketNotifications(bucketArn, isUnmanaged);

wdyt?

Copy link
Member

@ozelalisen ozelalisen Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand, how it would brake customer experience. It was added with this PR, so it did not exist before, it is a new method. Regarding best practices for grant pattern, this is not following the Design Guidelines, grant pattern should follow:

  • Name should have a “grant” prefix
  • Returns an iam.Grant object
  • First argument must be grantee: iam.IGrantable

I am fine to keep it in handler, but I think isUnmanaged property in method is not very clear from its name and best practice for grant should be followed. I would propose to rename it to something more clear, as this is exposed publicly. It is very clear for someone who knows the class, but as an exposed public api, it is not easy to understand from this name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ozelalisen Thank you for your patience and great review. After second consideration, I made it a private addHandlerPermissions() method and moved to the notifications-resource.ts. Let me know if you have any other concerns.

@pahud pahud requested a review from ozelalisen August 28, 2025 18:39
Copy link
Member

@ozelalisen ozelalisen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ozelalisen
Copy link
Member

@Mergifyio update

@mergify
Copy link
Contributor

mergify bot commented Sep 2, 2025

update

✅ Branch has been successfully updated

@ozelalisen
Copy link
Member

@Mergifyio requeue

@mergify
Copy link
Contributor

mergify bot commented Sep 2, 2025

requeue

✅ The queue state of this pull request has been cleaned. It can be re-embarked automatically

Copy link
Member

@Abogical Abogical left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissing review.

Copy link
Member

@Abogical Abogical left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissing my review.

@mergify
Copy link
Contributor

mergify bot commented Sep 3, 2025

Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify bot merged commit c0300d2 into aws:main Sep 3, 2025
19 of 20 checks passed
@github-actions
Copy link
Contributor

github-actions bot commented Sep 3, 2025

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.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 3, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug This issue is a bug. contribution/core This is a PR that came from AWS. effort/medium Medium work item – several days of effort p1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S3: BucketNotificationsHandler IAM Policy uses wildcard * by default

4 participants