Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-cloudfront-s3): update construct to use origin access controls; add support for CMK-encrypted buckets #1038

Merged
merged 50 commits into from
Jan 9, 2024

Conversation

hayesry
Copy link
Contributor

@hayesry hayesry commented Dec 8, 2023

Issue #, if available:
#831 #979

Description of changes:
This pull request updates the aws-cloudfront-s3 construct to use an origin access control (OAC) instead of an origin access identity (OAI) for accessing bucket that is either provisioned by or provided to the construct. This pull request also resolves an issue with the construct in working with CMK-encrypted buckets by adding a custom resource that updates the key policy whenever 1.) a CMK encryptionKey is provided as a bucketProp to be provisioned by the construct; or 2.) an existing bucket that is encrypted with a CMK is provided to the construct using the existingBucketObj property.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@@ -0,0 +1,109 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

This module needs to move to resources/lib/kms-key-policy-updater-custom-resource.ts - we've already got a structure for where to put custom resources.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this into the /resources folder in commit f2a7053

l1CloudFrontDistribution.addPropertyOverride('DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity', '');

// Grant CloudFront permission to get the objects from the s3 bucket origin
bucket.addToResourcePolicy(
Copy link
Contributor

Choose a reason for hiding this comment

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

let's change the name of this variable to originBucket

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7e82a23

// Define the OriginAccessControl
const originAccessControl = new cloudfront.CfnOriginAccessControl(this, 'CloudFrontOac', {
originAccessControlConfig: {
name: `cloudfront-default-oac-${new Date().getTime().toString(16)}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't work - name will be different each time it runs and cause issues with Stack Updates. Use GeneratePhysicalName() to get a reproducible unique name based on the Stack ID

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed this by using generatePhysicalName('aws-cloudfront-s3', ['oac'], 16) to create a unique name. Tested and answered the following questions before settling on this approach:

  • Does not specifying anything for name tell CloudFormation to give it a unique name? Didn't get that far, turns out its a required property by CDK, so that didn't work.
  • Does giving the OAC a general name like cloudfront-s3-oac tell CloudFormation to still make it unique? For example, cloudfront-s3-oac-stackNameHere-someOtherRandomValues? No, it's a literal property specification, so if you provide name: cloudfront-s3-oac, that will be the name. No adjustments or prefixes/suffixes added by CFN. This caused issues when running integration tests because one or more OACs were named the same, and the same issue would pop up if a customer had greater than one deployment of this construct.

);

// We need to create a custom resource to introduce the indirection necessary to avoid
// a circular dependency when granting the cloud front distribution access to use the
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to be sharing this A LOT in situations where people are discussing circular dependencies in CloudFormation, so let's change the comment to say CloudFront properly. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, updated in e0a323a

// * The S3 Bucket references the KMS Key
// * The CloudFront Distribution references the Bucket
// * The KMS Key references the CloudFront Distribution
let encryptionKey: IKey | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

A third option here needs to be enableEncryptionWithCustomerManagedKey?: boolean to be consistent with other Constructs that offer optional custom managed key functionality. See README entry in aws-lambda-sqs for one example.

Copy link
Contributor

Choose a reason for hiding this comment

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

Pinging this comment again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed together; going to hold off on this for now as we don't have a defined design pattern for taking CMK's from the top-level for use with encrypting buckets. Users can continue supplying their CMK via bucketProps if using the construct-provided bucket, or supplying their own encrypted bucket (either w/ CMK or AWS managed key) via existingBucketObj.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: githubautobuild-for-cdk-v2
  • Commit ID: e9b0063
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-solutions-constructs-team
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: githubautobuild-for-cdk-v2
  • Commit ID: 379fdd1
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@hayesry hayesry marked this pull request as ready for review January 5, 2024 03:36
@@ -21,10 +21,6 @@
## Overview
This AWS Solutions Construct provisions an Amazon CloudFront Distribution that serves objects from an AWS S3 Bucket via an Origin Access Control (OAC).

> **Note:** This AWS Solutions Construct was updated in December 2023 to replace the use of Origin Access Identities (OAIs) with Origin Access Controls (OACs) for accessing objects from the Bucket. Due to the
> current configuration of one or more underlying AWS CDK constructs, this Construct will still provison an OAI, but it will be "orphaned" and the OAC will be used for accessing the Bucket. The
Copy link
Contributor

Choose a reason for hiding this comment

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

This is no longer accurate with the use of S3OacOrigin, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, content removed as a result

@@ -113,28 +113,34 @@ export interface CloudFrontDistributionForS3Response {
/**
* @internal This is an internal core function and should not be called directly by Solutions Constructs clients.
*/
export function CloudFrontDistributionForS3(
export function createCloudFrontDistributionForS3(
scope: Construct,
sourceBucket: s3.IBucket,
cloudFrontDistributionProps?: cloudfront.DistributionProps | any,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we put the last 4 args into a CreateCloudFrontDistributionForS3Props interface? I'm not sure at what point SonarQube is going to yell at us for too many arguments, plus we use that pattern in many other locations - especially with a response interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 65e8c32

if (!sourceBucket.isWebsite) {
originAccessControl = new cloudfront.CfnOriginAccessControl(scope, 'CloudFrontOac', {
originAccessControlConfig: {
name: generatePhysicalName('aws-cloudfront-s3', ['oac'], 16),
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to add an ID to incoming arguments (not as part of the props interface) and include that in the physical name. Currently if a stack includes 2 instances of this construct we will have a name collision. Sent the ID sent to the construct constructor for the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 65e8c32

// If the bucket is configured for website hosting, set up an HttpOrigin to support legacy clients
printWarning(`Bucket ${bucket.bucketName} is being provided as an origin bucket to aws-cloudfront-s3, but currently
has static website hosting settings enabled. Please review whether this is the intended configuration. If so,
construct will configure an HttpOrigin to allow the distribution to access the bucket. If not, we strongly recommend
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the underlying issue here that the bucket and objects need to be public? We should call that out, then specifically say "AWS strongly recommends against public settings on S3 buckets and objects"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated language in 0b48016

@@ -43,13 +44,13 @@ export function createKeyPolicyUpdaterCustomResource(
lambdaFunctionProps: {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
description: 'kms-key-policy-updater',
description: 'Custom resource function that updates the key policy of an existing encryption key to allow CloudFront access.',
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

* and limitations under the License.
*/

import * as lambda from 'aws-cdk-lib/aws-lambda';
Copy link
Contributor

Choose a reason for hiding this comment

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

index doesn't seem like the correct name for this file - index.ts usually the top of the hierarchy in some way. This just looks like a utils file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can switch it to utils.ts, made the update in 545e005

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team

This comment was marked as outdated.

const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy);
const keyPolicyStatementSid: string = 'Grant-CloudFront-Distribution-Key-Usage';

if (checkForExistingKeyPolicyStatement(keyPolicy, keyPolicyStatementSid)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the sid already exists, but a new CMK is used, for example? We probably wouldn't want to return here in that case. Maybe the better logic would be to always overwrite the existing statement we are looking for, even if it ends up being a no-op .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 7f24545

Let me know if I'm missing anything with the implementation

*/
export const checkForExistingKeyPolicyStatement = (parsedKeyPolicy: any, sid: string) => {
const matches = parsedKeyPolicy.Statement.find((statement: any) => statement.Sid === sid);
return matches ? true : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit - you could just return matches here, as its already the boolean result

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 7f24545

lambdaFunctionProps: {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
description: 'Custom resource function that updates the key policy of an existing encryption key to allow CloudFront access.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the word existing here accurate? What if its a new CMK being provisioned as part of the construct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Touched this up in 7f24545

@aws-solutions-constructs-team

This comment was marked as outdated.

georgebearden
georgebearden previously approved these changes Jan 8, 2024
Copy link
Contributor

@georgebearden georgebearden left a comment

Choose a reason for hiding this comment

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

lgtm

props.insertHttpSecurityHeaders,
props.cloudFrontLoggingBucketProps,
props.responseHeadersPolicyProps
'cloudfront-dist-for-s3',
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing the ID gives us no benefit if we pass a constant value, we'll still get a name collision if we launch multiple constructs in 1 stack. The idea was to pass the id of this instance of the construct (the id argument to the constructor).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That... makes more sense. Switching it now.

@aws-solutions-constructs-team

This comment was marked as outdated.

@aws-solutions-constructs-team
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: githubautobuild-for-cdk-v2
  • Commit ID: 702ad2e
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@biffgaut biffgaut merged commit 012f9e7 into main Jan 9, 2024
1 check passed
@biffgaut biffgaut deleted the feature/hayesry/cloudfront-s3-oac-update-staging branch January 9, 2024 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants