diff --git a/RULES.md b/RULES.md index 007200df1b..8d55e9125c 100644 --- a/RULES.md +++ b/RULES.md @@ -54,7 +54,8 @@ The [AWS Solutions Library](https://aws.amazon.com/solutions/) offers a collecti | AwsSolutions-ATH1 | The Athena workgroup does not encrypt query results. | Encrypting query results stored in S3 helps secure data to meet compliance requirements for data-at-rest encryption. | | AwsSolutions-CB4 | The CodeBuild project does not use an AWS KMS key for encryption. | Using an AWS KMS key helps follow the standard security advice of granting least privilege to objects generated by the project. | | AwsSolutions-C91 | The Cloud9 instance does not use a no-ingress EC2 instance with AWS Systems Manager. | SSM adds an additional layer of protection as it allows operators to control access through IAM permissions and does not require opening inbound ports. | -| AwsSolutions-CFR3 | The CloudFront distributions does not have access logging enabled. | Enabling access logs helps operators track all viewer requests for the content delivered through the Content Delivery Network. | +| AwsSolutions-CFR3 | The CloudFront distribution does not have access logging enabled. | Enabling access logs helps operators track all viewer requests for the content delivered through the Content Delivery Network. | +| AwsSolutions-CFR4 | The CloudFront distribution allows for SSLv3 or TLSv1 for HTTPS viewer connections. | Vulnerabilities have been and continue to be discovered in the deprecated SSL and TLS protocols. Help protect viewer connections by specifying a viewer certificate that enforces a minimum of TLSv1.1 or TLSv1.2 in the security policy. Distributions that use that use the default CloudFront viewer certificate or use 'vip' for the `SslSupportMethod` are non-compliant with this rule, as the minimum security policy is set to TLSv1 regardless of the specified `MinimumProtocolVersion` | | AwsSolutions-CFR5 | The CloudFront distributions uses SSLv3 or TLSv1 for communication to the origin. | Vulnerabilities have been and continue to be discovered in the deprecated SSL and TLS protocols. Using a security policy with minimum TLSv1.1 or TLSv1.2 and appropriate security ciphers for HTTPS helps protect viewer connections. | | AwsSolutions-CFR6 | The CloudFront distribution does not use an origin access identity an S3 origin. | Origin access identities help with security by restricting any direct access to objects through S3 URLs. | | AwsSolutions-COG1 | The Cognito user pool does not have a password policy that minimally specify a password length of at least 8 characters, as well as requiring uppercase, numeric, and special characters. | Strong password policies increase system security by encouraging users to create reliable and secure passwords. | diff --git a/src/packs/aws-solutions.ts b/src/packs/aws-solutions.ts index ef5f5cee6f..d89d42f037 100644 --- a/src/packs/aws-solutions.ts +++ b/src/packs/aws-solutions.ts @@ -23,6 +23,7 @@ import { Cloud9InstanceNoIngressSystemsManager } from '../rules/cloud9'; import { CloudFrontDistributionAccessLogging, CloudFrontDistributionGeoRestrictions, + CloudFrontDistributionHttpsViewerNoOutdatedSSL, CloudFrontDistributionNoOutdatedSSL, CloudFrontDistributionS3OriginAccessIdentity, CloudFrontDistributionWAFIntegration, @@ -845,13 +846,22 @@ export class AwsSolutionsChecks extends NagPack { }); this.applyRule({ ruleSuffixOverride: 'CFR3', - info: 'The CloudFront distributions does not have access logging enabled.', + info: 'The CloudFront distribution does not have access logging enabled.', explanation: 'Enabling access logs helps operators track all viewer requests for the content delivered through the Content Delivery Network.', level: NagMessageLevel.ERROR, rule: CloudFrontDistributionAccessLogging, node: node, }); + this.applyRule({ + ruleSuffixOverride: 'CFR4', + info: 'The CloudFront distribution allows for SSLv3 or TLSv1 for HTTPS viewer connections.', + explanation: + "Vulnerabilities have been and continue to be discovered in the deprecated SSL and TLS protocols. Help protect viewer connections by specifying a viewer certificate that enforces a minimum of TLSv1.1 or TLSv1.2 in the security policy. Distributions that use that use the default CloudFront viewer certificate or use 'vip' for the 'SslSupportMethod' are non-compliant with this rule, as the minimum security policy is set to TLSv1 regardless of the specified 'MinimumProtocolVersion'.", + level: NagMessageLevel.ERROR, + rule: CloudFrontDistributionHttpsViewerNoOutdatedSSL, + node: node, + }); this.applyRule({ ruleSuffixOverride: 'CFR5', info: 'The CloudFront distributions uses SSLv3 or TLSv1 for communication to the origin.', diff --git a/src/rules/cloudfront/CloudFrontDistributionHttpsViewerNoOutdatedSSL.ts b/src/rules/cloudfront/CloudFrontDistributionHttpsViewerNoOutdatedSSL.ts new file mode 100644 index 0000000000..681f53a674 --- /dev/null +++ b/src/rules/cloudfront/CloudFrontDistributionHttpsViewerNoOutdatedSSL.ts @@ -0,0 +1,54 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnDistribution } from 'aws-cdk-lib/aws-cloudfront'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * CloudFront distributions use a security policy with minimum TLSv1.1 or TLSv1.2 and appropriate security ciphers for HTTPS viewer connections + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnDistribution) { + const distributionConfig = Stack.of(node).resolve( + node.distributionConfig + ); + const viewerCertificate = Stack.of(node).resolve( + distributionConfig.viewerCertificate + ); + if (viewerCertificate === undefined) { + return NagRuleCompliance.NON_COMPLIANT; + } + const minimumProtocolVersion = Stack.of(node).resolve( + viewerCertificate.minimumProtocolVersion + ); + const sslSupportMethod = Stack.of(node).resolve( + viewerCertificate.sslSupportMethod + ); + const cloudFrontDefaultCertificate = Stack.of(node).resolve( + viewerCertificate.cloudFrontDefaultCertificate + ); + const outdatedProtocols = ['SSLv3', 'TLSv1', 'TLSv1_2016']; + if ( + cloudFrontDefaultCertificate === true || + sslSupportMethod === undefined || + sslSupportMethod.toLowerCase() === 'vip' || + minimumProtocolVersion === undefined || + outdatedProtocols + .map((x) => x.toLowerCase()) + .includes(minimumProtocolVersion.toLowerCase()) + ) { + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.COMPLIANT; + } else { + return NagRuleCompliance.NOT_APPLICABLE; + } + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/cloudfront/index.ts b/src/rules/cloudfront/index.ts index 618311ecf7..d67d528893 100644 --- a/src/rules/cloudfront/index.ts +++ b/src/rules/cloudfront/index.ts @@ -4,6 +4,7 @@ SPDX-License-Identifier: Apache-2.0 */ export { default as CloudFrontDistributionAccessLogging } from './CloudFrontDistributionAccessLogging'; export { default as CloudFrontDistributionGeoRestrictions } from './CloudFrontDistributionGeoRestrictions'; +export { default as CloudFrontDistributionHttpsViewerNoOutdatedSSL } from './CloudFrontDistributionHttpsViewerNoOutdatedSSL'; export { default as CloudFrontDistributionNoOutdatedSSL } from './CloudFrontDistributionNoOutdatedSSL'; export { default as CloudFrontDistributionS3OriginAccessIdentity } from './CloudFrontDistributionS3OriginAccessIdentity'; export { default as CloudFrontDistributionWAFIntegration } from './CloudFrontDistributionWAFIntegration'; diff --git a/test/Packs.test.ts b/test/Packs.test.ts index 1250f224b4..c6c33604ff 100644 --- a/test/Packs.test.ts +++ b/test/Packs.test.ts @@ -75,6 +75,7 @@ describe('Check NagPack Details', () => { 'AwsSolutions-CB4', 'AwsSolutions-C91', 'AwsSolutions-CFR3', + 'AwsSolutions-CFR4', 'AwsSolutions-CFR5', 'AwsSolutions-CFR6', 'AwsSolutions-COG1', diff --git a/test/rules/CloudFront.test.ts b/test/rules/CloudFront.test.ts index ca8dbd5d23..fea978971d 100644 --- a/test/rules/CloudFront.test.ts +++ b/test/rules/CloudFront.test.ts @@ -2,6 +2,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Distribution, CfnDistribution, @@ -17,6 +18,7 @@ import { Aspects, Stack } from 'aws-cdk-lib/core'; import { CloudFrontDistributionAccessLogging, CloudFrontDistributionGeoRestrictions, + CloudFrontDistributionHttpsViewerNoOutdatedSSL, CloudFrontDistributionNoOutdatedSSL, CloudFrontDistributionS3OriginAccessIdentity, CloudFrontDistributionWAFIntegration, @@ -26,6 +28,7 @@ import { validateStack, TestType, TestPack } from './utils'; const testPack = new TestPack([ CloudFrontDistributionAccessLogging, CloudFrontDistributionGeoRestrictions, + CloudFrontDistributionHttpsViewerNoOutdatedSSL, CloudFrontDistributionNoOutdatedSSL, CloudFrontDistributionS3OriginAccessIdentity, CloudFrontDistributionWAFIntegration, @@ -38,8 +41,8 @@ beforeEach(() => { }); describe('Amazon CloudFront', () => { - describe('CloudFrontDistributionGeoRestrictions: CloudFront distributions may require Geo restrictions', () => { - const ruleId = 'CloudFrontDistributionGeoRestrictions'; + describe('CloudFrontDistributionAccessLogging: CloudFront distributions have access logging enabled', () => { + const ruleId = 'CloudFrontDistributionAccessLogging'; test('Noncompliance 1', () => { new Distribution(stack, 'rDistribution', { defaultBehavior: { @@ -48,31 +51,62 @@ describe('Amazon CloudFront', () => { }); validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); - test('Noncompliance 2', () => { - new CfnDistribution(stack, 'rDistribution', { - distributionConfig: { - restrictions: { geoRestriction: { restrictionType: 'none' } }, - enabled: false, + new CfnStreamingDistribution(stack, 'rStreamingDistribution', { + streamingDistributionConfig: { + comment: 'foo', + enabled: true, + s3Origin: { + domainName: 'foo.s3.us-east-1.amazonaws.com', + originAccessIdentity: + 'origin-access-identity/cloudfront/E127EXAMPLE51Z', + }, + trustedSigners: { + awsAccountNumbers: ['1111222233334444'], + enabled: true, + }, }, + tags: [{ key: 'foo', value: 'bar' }], }); validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); - test('Compliance', () => { + const logsBucket = new Bucket(stack, 'rLoggingBucket'); new Distribution(stack, 'rDistribution', { defaultBehavior: { origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), }, - geoRestriction: GeoRestriction.allowlist('US'), + logBucket: logsBucket, + }); + + new CfnStreamingDistribution(stack, 'rStreamingDistribution', { + streamingDistributionConfig: { + comment: 'foo', + enabled: true, + s3Origin: { + domainName: 'foo.s3.us-east-1.amazonaws.com', + originAccessIdentity: + 'origin-access-identity/cloudfront/E127EXAMPLE51Z', + }, + trustedSigners: { + awsAccountNumbers: ['1111222233334444'], + enabled: true, + }, + logging: { + bucket: logsBucket.bucketName, + prefix: 'foo', + enabled: true, + }, + }, + tags: [{ key: 'foo', value: 'bar' }], }); validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); - describe('CloudFrontDistributionWAFIntegration: CloudFront distributions may require integration with AWS WAF', () => { - const ruleId = 'CloudFrontDistributionWAFIntegration'; - test('Noncompliance ', () => { + describe('CloudFrontDistributionGeoRestrictions: CloudFront distributions may require Geo restrictions', () => { + const ruleId = 'CloudFrontDistributionGeoRestrictions'; + test('Noncompliance 1', () => { new Distribution(stack, 'rDistribution', { defaultBehavior: { origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), @@ -81,89 +115,102 @@ describe('Amazon CloudFront', () => { validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); + test('Noncompliance 2', () => { + new CfnDistribution(stack, 'rDistribution', { + distributionConfig: { + restrictions: { geoRestriction: { restrictionType: 'none' } }, + enabled: false, + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + test('Compliance', () => { new Distribution(stack, 'rDistribution', { defaultBehavior: { origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), }, - webAclId: new CfnWebACL(stack, 'rWebAcl', { - defaultAction: { - allow: { - customRequestHandling: { - insertHeaders: [{ name: 'foo', value: 'bar' }], - }, - }, - }, - scope: 'CLOUDFRONT', - visibilityConfig: { - cloudWatchMetricsEnabled: true, - metricName: 'foo', - sampledRequestsEnabled: true, - }, - }).attrId, + geoRestriction: GeoRestriction.allowlist('US'), }); validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); - describe('CloudFrontDistributionAccessLogging: CloudFront distributions have access logging enabled', () => { - const ruleId = 'CloudFrontDistributionAccessLogging'; - test('Noncompliance 1', () => { - new Distribution(stack, 'rDistribution', { - defaultBehavior: { - origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), + describe('CloudFrontDistributionHttpsViewerNoOutdatedSSL: CloudFront distributions use a security policy with minimum TLSv1.1 or TLSv1.2 and appropriate security ciphers for HTTPS viewer connections', () => { + const ruleId = 'CloudFrontDistributionHttpsViewerNoOutdatedSSL'; + test('Noncompliance 1: No viewer certificate specified', () => { + new CfnDistribution(stack, 'rDistribution', { + distributionConfig: { + enabled: true, }, }); validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); - test('Noncompliance 2', () => { - new CfnStreamingDistribution(stack, 'rStreamingDistribution', { - streamingDistributionConfig: { - comment: 'foo', + + test('Noncompliance 2: using the default CloudFront Viewer Certificate', () => { + new CfnDistribution(stack, 'rDistribution', { + distributionConfig: { enabled: true, - s3Origin: { - domainName: 'foo.s3.us-east-1.amazonaws.com', - originAccessIdentity: - 'origin-access-identity/cloudfront/E127EXAMPLE51Z', + viewerCertificate: { + cloudFrontDefaultCertificate: true, + minimumProtocolVersion: 'TLSv1.2_2019', + sslSupportMethod: 'sni-only', }, - trustedSigners: { - awsAccountNumbers: ['1111222233334444'], - enabled: true, + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 3: using an outdated protocol ', () => { + new CfnDistribution(stack, 'rDistribution', { + distributionConfig: { + enabled: true, + viewerCertificate: { + acmCertificateArn: + 'arn:aws:acm:us-east-1:111222333444:certificate/foo', + minimumProtocolVersion: 'SSLv3', + sslSupportMethod: 'sni-only', }, }, - tags: [{ key: 'foo', value: 'bar' }], }); validateStack(stack, ruleId, TestType.NON_COMPLIANCE); }); + + test('Noncompliance 3: using a virtual IP for ssl support ', () => { + new CfnDistribution(stack, 'rDistribution', { + distributionConfig: { + enabled: true, + viewerCertificate: { + acmCertificateArn: + 'arn:aws:acm:us-east-1:111222333444:certificate/foo', + minimumProtocolVersion: 'TLSv1.2_2019', + sslSupportMethod: 'vip', + }, + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + test('Compliance', () => { - const logsBucket = new Bucket(stack, 'rLoggingBucket'); new Distribution(stack, 'rDistribution', { + domainNames: ['foo.com'], defaultBehavior: { origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), }, - logBucket: logsBucket, + certificate: new Certificate(stack, 'rCertificate', { + domainName: 'foo.com', + }), }); - - new CfnStreamingDistribution(stack, 'rStreamingDistribution', { - streamingDistributionConfig: { - comment: 'foo', + new CfnDistribution(stack, 'rDistribution2', { + distributionConfig: { enabled: true, - s3Origin: { - domainName: 'foo.s3.us-east-1.amazonaws.com', - originAccessIdentity: - 'origin-access-identity/cloudfront/E127EXAMPLE51Z', - }, - trustedSigners: { - awsAccountNumbers: ['1111222233334444'], - enabled: true, - }, - logging: { - bucket: logsBucket.bucketName, - prefix: 'foo', - enabled: true, + viewerCertificate: { + acmCertificateArn: + 'arn:aws:acm:us-east-1:111222333444:certificate/foo', + minimumProtocolVersion: 'TLSv1.2_2019', + sslSupportMethod: 'sni-only', }, }, - tags: [{ key: 'foo', value: 'bar' }], }); validateStack(stack, ruleId, TestType.COMPLIANCE); }); @@ -286,4 +333,40 @@ describe('Amazon CloudFront', () => { validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); + + describe('CloudFrontDistributionWAFIntegration: CloudFront distributions may require integration with AWS WAF', () => { + const ruleId = 'CloudFrontDistributionWAFIntegration'; + test('Noncompliance ', () => { + new Distribution(stack, 'rDistribution', { + defaultBehavior: { + origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance', () => { + new Distribution(stack, 'rDistribution', { + defaultBehavior: { + origin: new S3Origin(new Bucket(stack, 'rOriginBucket')), + }, + webAclId: new CfnWebACL(stack, 'rWebAcl', { + defaultAction: { + allow: { + customRequestHandling: { + insertHeaders: [{ name: 'foo', value: 'bar' }], + }, + }, + }, + scope: 'CLOUDFRONT', + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: 'foo', + sampledRequestsEnabled: true, + }, + }).attrId, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); });