Skip to content

Commit

Permalink
add README section for sse-kms circular dependency workaround (#31262)
Browse files Browse the repository at this point in the history
- add README section with steps for user to follow to use escape hatch
to scope down the key policy
  • Loading branch information
gracelu0 authored Aug 30, 2024
2 parents afadeee + 762e036 commit 54d597e
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 180 deletions.
228 changes: 187 additions & 41 deletions packages/aws-cdk-lib/aws-cloudfront-origins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ and the distribution can use built-in S3 redirects and S3 custom error pages.
```ts
const myBucket = new s3.Bucket(this, 'myBucket');
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.S3StaticWebsiteOrigin(myBucket) },
defaultBehavior: { origin: new origins.S3StaticWebsiteOrigin({ bucket: myBucket }) },
});
```

Expand Down Expand Up @@ -73,6 +73,7 @@ When creating a S3 origin using `origins.S3BucketOrigin.withOriginAccessControl(
You can grant read, write or delete access to the OAC using the `originAccessLevels` property:

```ts
const myBucket = new s3.Bucket(this, 'myBucket');
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, {
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.WRITE, cloudfront.AccessLevel.DELETE],
});
Expand All @@ -85,7 +86,7 @@ const myBucket = new s3.Bucket(this, 'myBucket');
const oac = new cloudfront.S3OriginAccessControl(this, 'MyOAC', {
signing: cloudfront.Signing.SIGV4_NO_OVERRIDE
});
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(bucket, {
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, {
originAccessControl: oac
}
)
Expand All @@ -99,93 +100,234 @@ new cloudfront.Distribution(this, 'myDist', {
An existing S3 origin access control can be imported using the `fromOriginAccessControlId` method:

```ts
const importedOAC = cloudfront.S3OriginAccessControl.fromOriginAccessControlId(this, 'myImportedOAC', {
originAccessControlId: 'ABC123ABC123AB',
});
const importedOAC = cloudfront.S3OriginAccessControl.fromOriginAccessControlId(this, 'myImportedOAC', 'ABC123ABC123AB');
```

> [Note](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html): When you use OAC with S3
bucket origins, the bucket's object ownership must be set to Bucket owner enforced (default for new S3 buckets), or Bucket owner preferred (only if you require ACLs).

#### Setting up OAC with imported S3 buckets
#### Setting up OAC with a SSE-KMS encrypted S3 origin

If you are using an imported bucket for your S3 Origin and want to use OAC,
you will need to update
the S3 bucket policy manually to allow the OAC to access the S3 origin. Like most imported resources, CDK apps cannot modify the configuration of imported buckets.
If the objects in the S3 bucket origin are encrypted using server-side encryption with
AWS Key Management Service (SSE-KMS), the OAC must have permission to use the KMS key.

After deploying the distribution, add the following
policy statement to your
S3 bucket to allow CloudFront read-only access
(or additional S3 permissions as required):
Setting up an S3 origin using `S3BucketOrigin.withOriginAccessControl()` will automatically add the statement to the KMS key policy
to give the OAC permission to use the KMS key.

```ts
import * as kms from 'aws-cdk-lib/aws-kms';

const myKmsKey = new kms.Key(this, 'myKMSKey');
const myBucket = new s3.Bucket(this, 'mySSEKMSEncryptedBucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: myKmsKey,
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
});
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(myBucket) // Automatically grants Distribution access to `myKmsKey`
},
});
```

I saw this warning message during synth time. What do I do?

```text
To avoid circular dependency between the KMS key, Bucket, and Distribution,
a wildcard is used to match all Distribution IDs in Key policy condition.
To further scope down the policy for best security practices, see the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.
```

If the S3 bucket has an `encryptionKey` defined, `S3BucketOrigin.withOriginAccessControl()`
will automatically add the following policy statement to the KMS key policy to allow CloudFront read-only access (unless otherwise specified in the `originAccessLevels` property).

```json
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<S3 bucket name>/*",
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
"ArnLike": {
"AWS:SourceArn": "arn:aws:cloudfront::<account ID>:distribution/*"
}
}
}
}
```

See CloudFront docs on [Giving the origin access control permission to access the S3 bucket](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#create-oac-overview-s3) for more details.
This policy uses a wildcard to match all distribution IDs in the account instead of referencing the specific distribution ID to resolve the circular dependency. The policy statement is not as scoped down as the example in the AWS CloudFront docs (see [SSE-KMS section](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#create-oac-overview-s3)).

> Note: If your bucket previously used OAI, you will need to manually remove the policy statement
that gives the OAI access to your bucket after setting up OAC.
After you have deployed the Distribution, you should follow these steps to only grant permissions to the specific distribution according to AWS best practices:

#### Using OAC for a SSE-KMS encrypted S3 origin
**Step 1.** Copy the key policy

If the objects in the S3 bucket origin are encrypted using server-side encryption with
AWS Key Management Service (SSE-KMS), the OAC must have permission to use the KMS key.
Setting up an S3 origin using `S3BucketOrigin.withOriginAccessControl()` will automatically add the statement to the KMS key policy
to give the OAC permission to use the KMS key.
For imported keys, you will need to manually update the
key policy yourself as CDK apps cannot modify the configuration of imported resources.
**Step 2.** Use an escape hatch to update the policy statement condition so that

```json
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:cloudfront::<account ID>:distribution/*"
}
}
```

...becomes...

```json
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
}
}
```

> Note the change of condition operator from `ArnLike` to `StringEquals` in addition to replacing the wildcard (`*`) with the distribution ID.
To set the key policy using an escape hatch:

```ts
const myKmsKey = new kms.Key(this, 'myKMSKey');
import * as kms from 'aws-cdk-lib/aws-kms';

const kmsKey = new kms.Key(this, 'myKMSKey');
const myBucket = new s3.Bucket(this, 'mySSEKMSEncryptedBucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: kmsKey,
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
});
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(myBucket) // Automatically grants Distribution access to `myKmsKey`
origin: origins.S3BucketOrigin.withOriginAccessControl(myBucket)
},
});

// Add the following to scope down the key policy
const scopedDownKeyPolicy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
}
}
}
]
};
const cfnKey = (kmsKey.node.defaultChild as kms.CfnKey);
cfnKey.keyPolicy = scopedDownKeyPolicy;
```

If the S3 bucket has an `encryptionKey` defined, `S3BucketOrigin.withOriginAccessControl()`
will update the KMS key policy by appending the following policy statement to allow CloudFront read-only access (unless otherwise specified in the `originAccessLevels` property):
**Step 3.** Deploy the stack
> Tip: Run `cdk diff` before deploying to verify the
changes to your stack.

**Step 4.** Verify your final key policy includes the following statement after deploying:

```json
{
"Effect": "Allow",
"Principal": {
"Service": [
"cloudfront.amazonaws.com"
]
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
}
}
}
```

For imported keys, you will need to manually update the
key policy yourself as CDK apps cannot modify the configuration of imported resources. After deploying the distribution, add the following policy statement to your key policy to allow CloudFront OAC to access your KMS key for SSE-KMS:

```json
{
"Sid": "AllowCloudFrontServicePrincipalSSE-KMS",
"Effect": "Allow",
"Principal": {
"Service": [
"cloudfront.amazonaws.com"
]
},
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
}
}
}
```

#### Setting up OAC with imported S3 buckets

If you are using an imported bucket for your S3 Origin and want to use OAC,
you will need to update
the S3 bucket policy manually to allow the OAC to access the S3 origin. Like most imported resources, CDK apps cannot modify the configuration of imported buckets.

After deploying the distribution, add the following
policy statement to your
S3 bucket to allow CloudFront read-only access
(or additional S3 permissions as required):

```json
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:::key/<key ID>",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<S3 bucket name>/*",
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:cloudfront::<account ID>:distribution/*"
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID>"
}
}
}
}
```

See CloudFront docs on [Giving the origin access control permission to access the S3 bucket](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#create-oac-overview-s3) for more details.

> Note: If your bucket previously used OAI, you will need to manually remove the policy statement
that gives the OAI access to your bucket after setting up OAC.

#### Setting up an OAI (legacy)

Setup an S3 origin with origin access identity (legacy) as follows:
Expand All @@ -206,7 +348,7 @@ const myBucket = new s3.Bucket(this, 'myBucket');
const myOai = new cloudfront.OriginAccessIdentity(this, 'myOAI', {
comment: 'My custom OAI'
});
const s3Origin = origins.S3BucketOrigin.withOriginAccessIdentity(bucket, {
const s3Origin = origins.S3BucketOrigin.withOriginAccessIdentity(myBucket, {
originAccessIdentity: myOai
});
new cloudfront.Distribution(this, 'myDist', {
Expand Down Expand Up @@ -282,6 +424,10 @@ To ensure CloudFront doesn't lose access to the bucket during the transition, ad
changes to your stack.

```ts
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';

const stack = new Stack();
const myBucket = new s3.Bucket(this, 'myBucket');
const s3Origin = new origins.S3Origin(myBucket);
const distribution = new cloudfront.Distribution(this, 'myDist', {
Expand All @@ -306,7 +452,7 @@ const oacBucketPolicyStatement = new iam.PolicyStatement(
effect: iam.Effect.ALLOW,
principals: [cloudfrontSP],
actions: ['s3:GetObject'],
resources: [bucket.arnForObjects('*')],
resources: [myBucket.arnForObjects('*')],
conditions: {
"StringEquals": {
"AWS:SourceArn": distributionArn
Expand All @@ -316,7 +462,7 @@ const oacBucketPolicyStatement = new iam.PolicyStatement(
)

// Add statement to bucket policy
bucket.addToResourcePolicy(oacBucketPolicyStatement);
myBucket.addToResourcePolicy(oacBucketPolicyStatement);
```

The following changes will take place:
Expand All @@ -330,9 +476,9 @@ Replace `S3Origin` with `S3BucketOrigin.withOriginAccessControl()`, which create
Run `cdk diff` before deploying to verify the changes to your stack.

```ts
const bucket = new s3.Bucket(stack, 'Bucket');
const bucket = new s3.Bucket(this, 'Bucket');
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(bucket);
const distribution = new cloudfront.Distribution(stack, 'Distribution', {
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: s3Origin },
});
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const KEY_ACTIONS: Record<string, string[]> = {
/**
* Properties for configuring a origin using a standard S3 bucket
*/
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps {}
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps { }

/**
* Properties for configuring a S3 origin with OAC
Expand Down Expand Up @@ -156,7 +156,7 @@ export abstract class S3BucketOrigin extends cloudfront.OriginBase {
},
);
Annotations.of(key.node.scope!).addWarningV2('@aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac',
'To avoid circular dependency between the KMS key, Bucket, and Distribution,' +
'To avoid circular dependency between the KMS key, Bucket, and Distribution, ' +
'a wildcard is used to match all Distribution IDs in Key policy condition.\n' +
'To further scope down the policy for best security practices, see the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.');
const result = key.addToResourcePolicy(oacKeyPolicyStatement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ describe('S3BucketOrigin', () => {
},
});
Annotations.fromStack(stack).hasWarning('/Default',
'To avoid circular dependency between the KMS key, Bucket, and Distribution,' +
'To avoid circular dependency between the KMS key, Bucket, and Distribution, ' +
'a wildcard is used to match all Distribution IDs in Key policy condition.\n' +
'To further scope down the policy for best security practices, see the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README. [ack: @aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac]');
});
Expand Down Expand Up @@ -699,7 +699,7 @@ describe('S3BucketOrigin', () => {
it('should warn user bucket policy is not updated', () => {
Annotations.fromStack(stack).hasWarning('/Default/MyDistributionA/Origin1',
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac]');
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac]');
});

it('should match expected template resources', () => {
Expand Down Expand Up @@ -1151,7 +1151,7 @@ describe('S3BucketOrigin', () => {
it('should warn user bucket policy is not updated', () => {
Annotations.fromStack(distributionStack).hasWarning('/distributionStack/MyDistributionA/Origin1',
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai]');
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai]');
});

it('should create OAI in bucket stack and output it, then reference the output in the distribution stack', () => {
Expand Down
Loading

0 comments on commit 54d597e

Please sign in to comment.