From 8a5299a7360165ade6bb1fe55cb2d7bd28d7e293 Mon Sep 17 00:00:00 2001 From: ZeldoKavira Date: Fri, 28 Sep 2018 13:52:03 -0700 Subject: [PATCH] feat(aws-cloudfront): Support Security Policy (#804) Adds support for changing the default Security Policy (minimumProtocolVersion) with logic to ensure the proper one is being set for your SSLMethod. Fixes #795 --- .../aws-cloudfront/lib/web_distribution.ts | 85 ++++++++++++++++--- ...g.cloudfront-security-policy.expected.json | 65 ++++++++++++++ .../test/integ.cloudfront-security-policy.ts | 33 +++++++ 3 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.expected.json create mode 100644 packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index cd12299bb3e57..18db785426713 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -26,21 +26,45 @@ export enum ViewerProtocolPolicy { } /** - * CloudFront can use a custom domain that you provide instead of a "cloudfront.net" domain. - * To use this feature - you must provide the list of additional domains, - * and the ACM Certificate that CloudFront should use for these additional domains. + * Configuration for custom domain names * - * Note - CloudFront only accepts one additional certificate - therefore the certificate *must* - * use have SANs (Subject Alternative Names) for all domains listed. - * - * sslMethod is optional - we default to SNI if not specified. See the notes on SSLMethod if you wish to use other SSL termination types. - * - * @default sslMethod: SNI by default + * CloudFront can use a custom domain that you provide instead of a + * "cloudfront.net" domain. To use this feature you must provide the list of + * additional domains, and the ACM Certificate that CloudFront should use for + * these additional domains. */ export interface AliasConfiguration { - readonly names: string[], - readonly acmCertRef: string, - readonly sslMethod?: SSLMethod, + /** + * ARN of an AWS Certificate Manager (ACM) certificate. + */ + readonly acmCertRef: string; + + /** + * Domain names on the certificate + * + * Both main domain name and Subject Alternative Names. + */ + readonly names: string[]; + + /** + * How CloudFront should serve HTTPS requests. + * + * See the notes on SSLMethod if you wish to use other SSL termination types. + * + * @default SNI + * @see https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_ViewerCertificate.html + */ + readonly sslMethod?: SSLMethod; + + /** + * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. + * + * CloudFront serves your objects only to browsers or devices that support at + * least the SSL version that you specify. + * + * @default securityPolicy: SSLv3 if sslMethod VIP, TLSv1 if sslMethod SNI + */ + readonly securityPolicy?: SecurityPolicyProtocol; } /** @@ -64,6 +88,18 @@ export enum SSLMethod { VIP = "vip" } +/** + * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. + * CloudFront serves your objects only to browsers or devices that support at least the SSL version that you specify. + */ +export enum SecurityPolicyProtocol { + SSLv3 = "SSLv3", + TLSv1 = "TLSv1", + TLSv1_2016 = "TLSv1_2016", + TLSv1_1_2016 = "TLSv1.1_2016", + TLSv1_2_2018 = "TLSv1.2_2018" +} + /** * CloudFront supports logging of incoming requests and can log details to a given S3 Bucket. * @@ -453,6 +489,17 @@ export class CloudFrontWebDistribution extends cdk.Construct { ALL: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], }; + /** + * Maps for which SecurityPolicyProtocol are available to which SSLMethods + */ + private readonly VALID_SSL_PROTOCOLS: { [key: string]: string[] } = { + "sni-only": [ + SecurityPolicyProtocol.TLSv1, SecurityPolicyProtocol.TLSv1_1_2016, + SecurityPolicyProtocol.TLSv1_2016, SecurityPolicyProtocol.TLSv1_2_2018 + ], + "vip": [SecurityPolicyProtocol.SSLv3, SecurityPolicyProtocol.TLSv1], + }; + constructor(parent: cdk.Construct, name: string, props: CloudFrontWebDistributionProps) { super(parent, name); @@ -554,7 +601,21 @@ export class CloudFrontWebDistribution extends cdk.Construct { distributionConfig.viewerCertificate = { acmCertificateArn: props.aliasConfiguration.acmCertRef, sslSupportMethod: props.aliasConfiguration.sslMethod || SSLMethod.SNI, + minimumProtocolVersion: props.aliasConfiguration.securityPolicy }; + + if (distributionConfig.viewerCertificate.minimumProtocolVersion !== undefined) { + const validProtocols = this.VALID_SSL_PROTOCOLS[distributionConfig.viewerCertificate.sslSupportMethod!.toString()]; + + if (validProtocols === undefined) { + throw new Error(`Invalid sslMethod. ${distributionConfig.viewerCertificate.sslSupportMethod!.toString()} is not fully implemented yet.`); + } + + if (validProtocols.indexOf(distributionConfig.viewerCertificate.minimumProtocolVersion.toString()) === -1) { + // tslint:disable-next-line:max-line-length + throw new Error(`${distributionConfig.viewerCertificate.minimumProtocolVersion} is not compabtible with sslMethod ${distributionConfig.viewerCertificate.sslSupportMethod}.\n\tValid Protocols are: ${validProtocols.join(", ")}`); + } + } } else { distributionConfig.viewerCertificate = { cloudFrontDefaultCertificate: true diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.expected.json new file mode 100644 index 0000000000000..cf27f2139b9f2 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.expected.json @@ -0,0 +1,65 @@ +{ + "Resources": { + "AnAmazingWebsiteProbablyCFDistribution47E3983B": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "CacheBehaviors": [], + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": [ + "TLSv1.2" + ] + }, + "DomainName": "brelandm.a2z.com", + "Id": "origin1", + "OriginCustomHeaders": [ + { + "HeaderName": "X-Custom-Header", + "HeaderValue": "somevalue" + } + ] + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "AcmCertificateArn": "testACM", + "MinimumProtocolVersion": "TLSv1", + "SslSupportMethod": "sni-only" + }, + "Aliases": [ + "test.test.com" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.ts new file mode 100644 index 0000000000000..86d0019de1198 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-security-policy.ts @@ -0,0 +1,33 @@ + +import cdk = require('@aws-cdk/cdk'); +import cloudfront = require('../lib'); + +const app = new cdk.App(process.argv); + +const stack = new cdk.Stack(app, 'aws-cdk-cloudfront-custom'); + +new cloudfront.CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { + originConfigs: [ + { + originHeaders: { + "X-Custom-Header": "somevalue", + }, + customOriginSource: { + domainName: "brelandm.a2z.com", + }, + behaviors: [ + { + isDefaultBehavior: true, + } + ] + } + ], + aliasConfiguration: { + acmCertRef: 'testACM', + names: ['test.test.com'], + sslMethod: cloudfront.SSLMethod.SNI, + securityPolicy: cloudfront.SecurityPolicyProtocol.TLSv1 + } +}); + +process.stdout.write(app.run());