diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 1355d7a64d31d..3a5834d98d51f 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -239,6 +239,34 @@ new cloudfront.Distribution(this, 'myDistCustomPolicy', { }); ``` +### Validating signed URLs or signed cookies with Trusted Key Groups + +CloudFront Distribution now supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. + +Example: + +```ts +// public key in PEM format +const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, +}); + +const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], +}); + +new cloudfront.Distribution(stack, 'Dist', { + defaultBehavior: { + origin: new origins.HttpOrigin('www.example.com'), + trustedKeyGroups: [ + keyGroup, + ], + }, +}); +``` + ### Lambda@Edge Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers. @@ -274,7 +302,7 @@ new cloudfront.Distribution(this, 'myDist', { > **Note:** Lambda@Edge functions must be created in the `us-east-1` region, regardless of the region of the CloudFront distribution and stack. > To make it easier to request functions for Lambda@Edge, the `EdgeFunction` construct can be used. > The `EdgeFunction` construct will automatically request a function in `us-east-1`, regardless of the region of the current stack. -> `EdgeFunction` has the same interface as `Function` and can be created and used interchangably. +> `EdgeFunction` has the same interface as `Function` and can be created and used interchangeably. > Please note that using `EdgeFunction` requires that the `us-east-1` region has been bootstrapped. > See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more about bootstrapping regions. @@ -289,7 +317,7 @@ const myFunc = new lambda.Function(this, 'MyFunction', { ``` If the stack is not in `us-east-1`, and you need references from different applications on the same account, -you can also set a specific stack ID for each Lamba@Edge. +you can also set a specific stack ID for each Lambda@Edge. ```ts const myFunc1 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction1', { @@ -427,7 +455,7 @@ You can customize the default certificate aliases. This is intended to be used i Example: -[create a distrubution with an default certificiate example](test/example.default-cert-alias.lit.ts) +[create a distribution with an default certificate example](test/example.default-cert-alias.lit.ts) #### ACM certificate @@ -438,7 +466,7 @@ For more information, see [the aws-certificatemanager module documentation](http Example: -[create a distrubution with an acm certificate example](test/example.acm-cert-alias.lit.ts) +[create a distribution with an acm certificate example](test/example.acm-cert-alias.lit.ts) #### IAM certificate @@ -448,7 +476,43 @@ See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFr Example: -[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts) +[create a distribution with an iam certificate example](test/example.iam-cert-alias.lit.ts) + +### Trusted Key Groups + +CloudFront Web Distributions supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. + +Example: + +```ts +const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, +}); + +const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], +}); + +new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors: [ + { + isDefaultBehavior: true, + trustedKeyGroups: [ + keyGroup, + ], + }, + ], + }, + ], +}); +``` ### Restrictions @@ -505,7 +569,7 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { }, failoverS3OriginSource: { s3BucketSource: s3.Bucket.fromBucketName(stack, 'aBucketFallback', 'myoriginbucketfallback'), - originPath: '/somwhere', + originPath: '/somewhere', originHeaders: { 'myHeader2': '21', }, diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 05534862026ae..02e3d092295f3 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -6,6 +6,7 @@ import { Construct } from 'constructs'; import { ICachePolicy } from './cache-policy'; import { CfnDistribution } from './cloudfront.generated'; import { GeoRestriction } from './geo-restriction'; +import { IKeyGroup } from './key-group'; import { IOrigin, OriginBindConfig, OriginBindOptions } from './origin'; import { IOriginRequestPolicy } from './origin-request-policy'; import { CacheBehavior } from './private/cache-behavior'; @@ -706,6 +707,14 @@ export interface AddBehaviorOptions { * @see https://aws.amazon.com/lambda/edge */ readonly edgeLambdas?: EdgeLambda[]; + + /** + * A list of Key Groups that CloudFront can use to validate signed URLs or signed cookies. + * + * @default - no KeyGroups are associated with cache behavior + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + */ + readonly trustedKeyGroups?: IKeyGroup[]; } /** diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 4b700e166cecc..d804dd8465750 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -50,13 +50,12 @@ export class CacheBehavior { originRequestPolicyId: this.props.originRequestPolicy?.originRequestPolicyId, smoothStreaming: this.props.smoothStreaming, viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL, - lambdaFunctionAssociations: this.props.edgeLambdas - ? this.props.edgeLambdas.map(edgeLambda => ({ - lambdaFunctionArn: edgeLambda.functionVersion.edgeArn, - eventType: edgeLambda.eventType.toString(), - includeBody: edgeLambda.includeBody, - })) - : undefined, + lambdaFunctionAssociations: this.props.edgeLambdas?.map(edgeLambda => ({ + lambdaFunctionArn: edgeLambda.functionVersion.edgeArn, + eventType: edgeLambda.eventType.toString(), + includeBody: edgeLambda.includeBody, + })), + trustedKeyGroups: this.props.trustedKeyGroups?.map(keyGroup => keyGroup.keyGroupId), }; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts index c62e1e09ed3f4..ae3cbae6051d3 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts @@ -7,6 +7,7 @@ import { Construct } from 'constructs'; import { CfnDistribution } from './cloudfront.generated'; import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { GeoRestriction } from './geo-restriction'; +import { IKeyGroup } from './key-group'; import { IOriginAccessIdentity } from './origin-access-identity'; /** @@ -347,9 +348,18 @@ export interface Behavior { * The signers are the account IDs that are allowed to sign cookies/presigned URLs for this distribution. * * If you pass a non empty value, all requests for this behavior must be signed (no public access will be allowed) + * @deprecated - We recommend using trustedKeyGroups instead of trustedSigners. */ readonly trustedSigners?: string[]; + /** + * A list of Key Groups that CloudFront can use to validate signed URLs or signed cookies. + * + * @default - no KeyGroups are associated with cache behavior + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + */ + readonly trustedKeyGroups?: IKeyGroup[]; + /** * * The default amount of time CloudFront will cache an object. @@ -932,6 +942,7 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu forwardedValues: input.forwardedValues || { queryString: false, cookies: { forward: 'none' } }, maxTtl: input.maxTtl && input.maxTtl.toSeconds(), minTtl: input.minTtl && input.minTtl.toSeconds(), + trustedKeyGroups: input.trustedKeyGroups?.map(key => key.keyGroupId), trustedSigners: input.trustedSigners, targetOriginId: input.targetOriginId, viewerProtocolPolicy: protoPolicy || ViewerProtocolPolicy.REDIRECT_TO_HTTPS, diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json new file mode 100644 index 0000000000000..afae558dee709 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "MyPublicKey78071F3D": { + "Type": "AWS::CloudFront::PublicKey", + "Properties": { + "PublicKeyConfig": { + "CallerReference": "c8752fac3fe06fc93f3fbd12d7e0282d8967409e4d", + "EncodedKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS\nJAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa\ndlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj\n6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e\n0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD\n/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx\nNQIDAQAB\n-----END PUBLIC KEY-----", + "Name": "integdistributionkeygroupMyPublicKeyC0F3B115" + } + } + }, + "MyKeyGroupAF22FD35": { + "Type": "AWS::CloudFront::KeyGroup", + "Properties": { + "KeyGroupConfig": { + "Items": [ + { + "Ref": "MyPublicKey78071F3D" + } + ], + "Name": "integdistributionkeygroupMyKeyGroupF179E01A" + } + } + }, + "DistB3B78991": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "Compress": true, + "TargetOriginId": "integdistributionkeygroupDistOrigin1B9677703", + "TrustedKeyGroups": [ + { + "Ref": "MyKeyGroupAF22FD35" + } + ], + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "CustomOriginConfig": { + "OriginProtocolPolicy": "https-only" + }, + "DomainName": "www.example.com", + "Id": "integdistributionkeygroupDistOrigin1B9677703" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts new file mode 100644 index 0000000000000..e0a124b26f904 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts @@ -0,0 +1,32 @@ +import * as cdk from '@aws-cdk/core'; +import * as cloudfront from '../lib'; +import { TestOrigin } from './test-origin'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-distribution-key-group'); +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +new cloudfront.Distribution(stack, 'Dist', { + defaultBehavior: { + origin: new TestOrigin('www.example.com'), + trustedKeyGroups: [ + new cloudfront.KeyGroup(stack, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }), + ], + }), + ], + }, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index a1e315fdcdd96..c9500b23eaddf 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,11 +1,20 @@ import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods, CachedMethods, CachePolicy, LambdaEdgeEventType, OriginRequestPolicy, ViewerProtocolPolicy } from '../../lib'; +import { AllowedMethods, CachedMethods, CachePolicy, KeyGroup, LambdaEdgeEventType, OriginRequestPolicy, PublicKey, ViewerProtocolPolicy } from '../../lib'; import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; let stack: Stack; +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; beforeEach(() => { app = new App(); @@ -30,6 +39,15 @@ test('renders the minimum template with an origin and path specified', () => { test('renders with all properties specified', () => { const fnVersion = lambda.Version.fromVersionArn(stack, 'Version', 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1'); + const pubKey = new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }); + const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], + }); + const behavior = new CacheBehavior('origin_id', { pathPattern: '*', allowedMethods: AllowedMethods.ALLOW_ALL, @@ -44,6 +62,7 @@ test('renders with all properties specified', () => { includeBody: true, functionVersion: fnVersion, }], + trustedKeyGroups: [keyGroup], }); expect(behavior._renderBehavior()).toEqual({ @@ -61,6 +80,9 @@ test('renders with all properties specified', () => { eventType: 'origin-request', includeBody: true, }], + trustedKeyGroups: [ + keyGroup.keyGroupId, + ], }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts index ea2fbf3f22295..8eea2cad5bc92 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts @@ -8,7 +8,9 @@ import { CfnDistribution, CloudFrontWebDistribution, GeoRestriction, + KeyGroup, LambdaEdgeEventType, + PublicKey, SecurityPolicyProtocol, SSLMethod, ViewerCertificate, @@ -17,6 +19,16 @@ import { /* eslint-disable quote-props */ +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + nodeunitShim({ 'distribution with custom origin adds custom origin'(test: Test) { @@ -188,6 +200,14 @@ nodeunitShim({ 'distribution with trusted signers on default distribution'(test: Test) { const stack = new cdk.Stack(); const sourceBucket = new s3.Bucket(stack, 'Bucket'); + const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, + }); + const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], + }); new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { originConfigs: [ @@ -199,6 +219,9 @@ nodeunitShim({ { isDefaultBehavior: true, trustedSigners: ['1234'], + trustedKeyGroups: [ + keyGroup, + ], }, ], }, @@ -212,6 +235,29 @@ nodeunitShim({ 'DeletionPolicy': 'Retain', 'UpdateReplacePolicy': 'Retain', }, + 'MyPubKey6ADA4CF5': { + 'Type': 'AWS::CloudFront::PublicKey', + 'Properties': { + 'PublicKeyConfig': { + 'CallerReference': 'c8141e732ea37b19375d4cbef2b2d2c6f613f0649a', + 'EncodedKey': publicKey, + 'Name': 'MyPubKey', + }, + }, + }, + 'MyKeyGroupAF22FD35': { + 'Type': 'AWS::CloudFront::KeyGroup', + 'Properties': { + 'KeyGroupConfig': { + 'Items': [ + { + 'Ref': 'MyPubKey6ADA4CF5', + }, + ], + 'Name': 'MyKeyGroup', + }, + }, + }, 'AnAmazingWebsiteProbablyCFDistribution47E3983B': { 'Type': 'AWS::CloudFront::Distribution', 'Properties': { @@ -250,9 +296,12 @@ nodeunitShim({ 'QueryString': false, 'Cookies': { 'Forward': 'none' }, }, - 'TrustedSigners': [ - '1234', + 'TrustedKeyGroups': [ + { + 'Ref': 'MyKeyGroupAF22FD35', + }, ], + 'TrustedSigners': ['1234'], 'Compress': true, }, 'Enabled': true,