-
Notifications
You must be signed in to change notification settings - Fork 251
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
Changes from 7 commits
1f4eda2
d39a81d
01a834e
372a244
a02f795
a1e9872
2b7fc52
f2a7053
2fbce66
7e82a23
721d2ad
c231611
b7b583d
dac63df
16519f9
e08a746
1bbe291
ba5549f
2b35b85
eeee5bb
91cb42d
aa6a554
572b05b
2b78680
32bb658
2b4a67f
aafa8dd
9f41fe6
721725b
ad63fe5
e054453
e0a323a
d503152
5f73d6a
f2ececd
744841c
0b77299
fde2da5
49b8369
9b231fc
e9b0063
379fdd1
65e8c32
e512955
0b48016
545e005
aa61318
7f24545
7acf926
702ad2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
lib/*.js | ||
lib/**/*.js | ||
test/*.js | ||
*.d.ts | ||
coverage | ||
test/lambda/index.js | ||
node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
**/*.js | ||
node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved this into the |
||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance | ||
* with the License. A copy of the License is located at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES | ||
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions | ||
* and limitations under the License. | ||
*/ | ||
|
||
import { KMSClient, GetKeyPolicyCommand, DescribeKeyCommand, PutKeyPolicyCommand, KeyManagerType } from "@aws-sdk/client-kms"; | ||
|
||
const kmsClient = new KMSClient(); | ||
|
||
export const handler = async (e: any, context: any) => { | ||
|
||
let status = 'SUCCESS'; | ||
let responseData = {}; | ||
|
||
if (e.RequestType === 'Create' || e.RequestType === 'Update') { | ||
|
||
try { | ||
const kmsKeyId = e.ResourceProperties.KmsKeyId; | ||
const cloudFrontDistributionId = e.ResourceProperties.CloudFrontDistributionId; | ||
const accountId = e.ResourceProperties.AccountId; | ||
const region = process.env.AWS_REGION; | ||
|
||
const describeKeyCommandResponse = await kmsClient.send(new DescribeKeyCommand({ | ||
KeyId: kmsKeyId | ||
})); | ||
|
||
if (describeKeyCommandResponse.KeyMetadata?.KeyManager === KeyManagerType.AWS) { | ||
return { | ||
Status: 'SUCCESS', | ||
Reason: 'An AWS managed key was provided, no action needed from the custom resource, exiting now.', | ||
PhysicalResourceId: e.PhysicalResourceId ?? context.logStreamName, | ||
StackId: e.StackId, | ||
RequestId: e.RequestId, | ||
LogicalResourceId: e.LogicalResourceId, | ||
Data: 'An AWS managed key was provided, no action needed from the custom resource, exiting now.', | ||
}; | ||
} | ||
|
||
const getKeyPolicyCommandResponse = await kmsClient.send(new GetKeyPolicyCommand({ | ||
KeyId: kmsKeyId, | ||
PolicyName: 'default' | ||
})); | ||
|
||
if (!getKeyPolicyCommandResponse.Policy) { | ||
return { | ||
Status: 'FAILED', | ||
Reason: 'An error occurred while retrieving the key policy', | ||
PhysicalResourceId: e.PhysicalResourceId ?? context.logStreamName, | ||
StackId: e.StackId, | ||
RequestId: e.RequestId, | ||
LogicalResourceId: e.LogicalResourceId, | ||
Data: 'An error occurred while retrieving the key policy', | ||
}; | ||
} | ||
|
||
const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy); | ||
|
||
// Update the existing key policy to allow the cloudfront distribution to use the key | ||
keyPolicy.Statement.push({ | ||
Sid: 'Grant-CloudFront-Distribution-Key-Usage', | ||
Effect: 'Allow', | ||
Principal: { | ||
Service: 'cloudfront.amazonaws.com', | ||
}, | ||
Action: [ | ||
'kms:Decrypt', | ||
'kms:Encrypt', | ||
'kms:GenerateDataKey*', | ||
'kms:ReEncrypt*' | ||
], | ||
Resource: `arn:aws:kms:${region}:${accountId}:key/${kmsKeyId}`, | ||
Condition: { | ||
StringEquals: { | ||
'AWS:SourceArn': `arn:aws:cloudfront::${accountId}:distribution/${cloudFrontDistributionId}` | ||
} | ||
} | ||
}); | ||
|
||
await kmsClient.send(new PutKeyPolicyCommand({ | ||
KeyId: kmsKeyId, | ||
Policy: JSON.stringify(keyPolicy), | ||
PolicyName: 'default' | ||
})); | ||
} catch (err) { | ||
status = 'FAILED'; | ||
responseData = { | ||
Error: JSON.stringify(err) | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
Status: status, | ||
Reason: JSON.stringify(responseData), | ||
PhysicalResourceId: e.PhysicalResourceId ?? context.logStreamName, | ||
StackId: e.StackId, | ||
RequestId: e.RequestId, | ||
LogicalResourceId: e.LogicalResourceId, | ||
Data: responseData, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"name": "kms-key-policy-updater", | ||
"version": "0.0.0", | ||
"description": "Custom resource for updating CMK key policies", | ||
"main": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"author": { | ||
"name": "Amazon Web Services", | ||
"url": "https://aws.amazon.com", | ||
"organization": true | ||
}, | ||
"license": "Apache-2.0", | ||
"scripts": { | ||
"build": "tsc -b .", | ||
"lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", | ||
"lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", | ||
"test": "jest --coverage", | ||
"clean": "tsc -b --clean", | ||
"watch": "tsc -b -w" | ||
}, | ||
"dependencies": { | ||
"@aws-sdk/client-kms": "^3.468.0", | ||
"aws-sdk-client-mock": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^27.4.0", | ||
"@types/node": "^10.3.0" | ||
}, | ||
"jest": { | ||
"moduleFileExtensions": [ | ||
"js" | ||
], | ||
"coverageReporters": [ | ||
"text", | ||
[ | ||
"lcov", | ||
{ | ||
"projectRoot": "../../../../" | ||
} | ||
] | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance | ||
* with the License. A copy of the License is located at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES | ||
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions | ||
* and limitations under the License. | ||
*/ | ||
|
||
import { handler } from ".."; | ||
import { mockClient } from "aws-sdk-client-mock"; | ||
import { KMSClient, DescribeKeyCommand, KeyManagerType, GetKeyPolicyCommand, PutKeyPolicyCommand } from "@aws-sdk/client-kms"; | ||
|
||
const kmsMock = mockClient(KMSClient); | ||
|
||
beforeEach(() => { | ||
kmsMock.reset(); | ||
}); | ||
|
||
it('Should exit if an AWS managed key is provided; return a success code but give the exact reason', async () => { | ||
// Mocks | ||
kmsMock.on(DescribeKeyCommand).resolves({ | ||
KeyMetadata: { | ||
KeyId: 'sample-key-id', | ||
KeyManager: KeyManagerType.AWS | ||
} | ||
}); | ||
// Arrange | ||
const e = { | ||
RequestType: 'Create', | ||
ResourceProperties: { | ||
CloudFrontDistributionId: 'sample-cf-distro-id', | ||
AccountId: '111122223333' | ||
} | ||
}; | ||
const context = { | ||
// ... | ||
}; | ||
// Act | ||
const res = await handler(e, context); | ||
// Assert | ||
expect(res.Status).toBe('SUCCESS'); | ||
expect(res.Data).toBe('An AWS managed key was provided, no action needed from the custom resource, exiting now.'); | ||
}); | ||
|
||
it('Should return an error if the key policy is returned as undefined', async () => { | ||
// Mocks | ||
kmsMock.on(DescribeKeyCommand).resolves({ | ||
KeyMetadata: { | ||
KeyId: 'sample-key-id', | ||
KeyManager: KeyManagerType.CUSTOMER | ||
} | ||
}); | ||
kmsMock.on(GetKeyPolicyCommand).resolves({ | ||
Policy: undefined | ||
}); | ||
// Arrange | ||
const e = { | ||
RequestType: 'Update', | ||
ResourceProperties: { | ||
CloudFrontDistributionId: 'sample-cf-distro-id', | ||
AccountId: '111122223333' | ||
} | ||
}; | ||
const context = { | ||
// ... | ||
}; | ||
// Act | ||
const res = await handler(e, context); | ||
// Assert | ||
expect(res.Status).toBe('FAILED'); | ||
}); | ||
|
||
it('Should update the key policy if the proper params are given', async () => { | ||
// Mocks | ||
kmsMock.on(DescribeKeyCommand).resolves({ | ||
KeyMetadata: { | ||
KeyId: 'sample-key-id', | ||
KeyManager: KeyManagerType.CUSTOMER | ||
} | ||
}); | ||
kmsMock.on(GetKeyPolicyCommand).resolves({ | ||
Policy: `{\n | ||
\"Version\" : \"2012-10-17\",\n | ||
\"Id\" : \"key-default-1\",\n | ||
\"Statement\" : [ {\n | ||
\"Sid\" : \"Enable IAM User Permissions\",\n | ||
\"Effect\" : \"Allow\",\n | ||
\"Principal\" : {\n | ||
\"AWS\" : \"arn:aws:iam::111122223333:root\"\n | ||
},\n | ||
\"Action\" : \"kms:*\",\n | ||
\"Resource\" : \"*\"\n | ||
} ]\n | ||
}` | ||
}); | ||
// Arrange | ||
const e = { | ||
RequestType: 'Update', | ||
ResourceProperties: { | ||
CloudFrontDistributionId: 'sample-cf-distro-id', | ||
AccountId: '111122223333' | ||
} | ||
}; | ||
const context = { | ||
// ... | ||
}; | ||
// Act | ||
const res = await handler(e, context); | ||
// Assert | ||
expect(res.Status).toBe('SUCCESS'); | ||
}); | ||
|
||
it('Should fail if an error occurs with putting the new key policy, all other inputs valid', async () => { | ||
// Mocks | ||
kmsMock.on(DescribeKeyCommand).resolves({ | ||
KeyMetadata: { | ||
KeyId: 'sample-key-id', | ||
KeyManager: KeyManagerType.CUSTOMER | ||
} | ||
}); | ||
kmsMock.on(GetKeyPolicyCommand).resolves({ | ||
Policy: `{\n | ||
\"Version\" : \"2012-10-17\",\n | ||
\"Id\" : \"key-default-1\",\n | ||
\"Statement\" : [ {\n | ||
\"Sid\" : \"Enable IAM User Permissions\",\n | ||
\"Effect\" : \"Allow\",\n | ||
\"Principal\" : {\n | ||
\"AWS\" : \"arn:aws:iam::111122223333:root\"\n | ||
},\n | ||
\"Action\" : \"kms:*\",\n | ||
\"Resource\" : \"*\"\n | ||
} ]\n | ||
}` | ||
}); | ||
kmsMock.on(PutKeyPolicyCommand).rejects(); | ||
const e = { | ||
RequestType: 'Update', | ||
ResourceProperties: { | ||
CloudFrontDistributionId: 'sample-cf-distro-id', | ||
AccountId: '111122223333' | ||
} | ||
}; | ||
const context = { | ||
// ... | ||
}; | ||
// Act | ||
const res = await handler(e, context); | ||
// Assert | ||
expect(res.Status).toBe('FAILED'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still true? With the new S3OacOrigin construct, my anticipation would be that upgrading old versions will leave behind an OAI, but new stacks will not get one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, removed the notice altogether in e0a323a since we now just provision an OAC and no OAI