@aws/nx-plugin 1.0.0-rc.40 → 1.0.0-rc.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,21 @@
1
- import { CfnOutput, CfnResource, Duration, Lazy, RemovalPolicy, Stack } from 'aws-cdk-lib';
1
+ import {
2
+ CfnOutput,
3
+ CfnResource,
4
+ Duration,
5
+ Lazy,
6
+ Names,
7
+ RemovalPolicy,
8
+ Stack,
9
+ } from 'aws-cdk-lib';
2
10
  import {
3
11
  Distribution,
4
12
  HeadersFrameOption,
5
13
  HeadersReferrerPolicy,
6
14
  ResponseHeadersPolicy,
15
+ SecurityPolicyProtocol,
7
16
  ViewerProtocolPolicy,
8
17
  } from 'aws-cdk-lib/aws-cloudfront';
18
+ import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
9
19
  import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
10
20
  import {
11
21
  BlockPublicAccess,
@@ -22,6 +32,14 @@ import {
22
32
  import { Construct } from 'constructs';
23
33
  import { RuntimeConfig } from './runtime-config<% if (esm) { %>.js<% } %>';
24
34
  import { Key } from 'aws-cdk-lib/aws-kms';
35
+ import {
36
+ CfnDelivery,
37
+ CfnDeliveryDestination,
38
+ CfnDeliverySource,
39
+ LogGroup,
40
+ RetentionDays,
41
+ } from 'aws-cdk-lib/aws-logs';
42
+ import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
25
43
  import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
26
44
  import { suppressRules } from './checkov<% if (esm) { %>.js<% } %>';
27
45
 
@@ -47,6 +65,15 @@ const CONTENT_SECURITY_POLICY = [
47
65
  export interface StaticWebsiteProps {
48
66
  readonly websiteName: string;
49
67
  readonly websiteFilePath: string;
68
+ /**
69
+ * Custom domain names for the CloudFront distribution. Requires `certificate`.
70
+ */
71
+ readonly domainNames?: string[];
72
+ /**
73
+ * ACM certificate for the custom domain names. Must be in us-east-1.
74
+ * When provided, viewers are required to use TLS 1.2 or later.
75
+ */
76
+ readonly certificate?: ICertificate;
50
77
  }
51
78
 
52
79
  /**
@@ -65,39 +92,43 @@ export class StaticWebsite extends Construct {
65
92
  constructor(
66
93
  scope: Construct,
67
94
  id: string,
68
- { websiteFilePath, websiteName }: StaticWebsiteProps
95
+ { websiteFilePath, websiteName, domainNames, certificate }: StaticWebsiteProps
69
96
  ) {
70
97
  super(scope, id);
71
- this.node.setContext(
72
- '@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy',
73
- true
74
- );
75
98
 
76
99
  const websiteKey = new Key(this, 'WebsiteKey', {
77
100
  enableKeyRotation: true,
78
101
  });
79
102
 
80
- const accessLogsBucket = new Bucket(this, 'AccessLogsBucket', {
81
- versioned: false,
82
- enforceSSL: true,
83
- autoDeleteObjects: true,
84
- removalPolicy: RemovalPolicy.DESTROY,
85
- encryption: BucketEncryption.KMS,
103
+ // Allow CloudWatch Logs to use the website key for server access log delivery.
104
+ const stack = Stack.of(this);
105
+ websiteKey.addToResourcePolicy(
106
+ new PolicyStatement({
107
+ effect: Effect.ALLOW,
108
+ principals: [
109
+ new ServicePrincipal(`logs.${stack.region}.amazonaws.com`),
110
+ ],
111
+ actions: [
112
+ 'kms:Encrypt',
113
+ 'kms:Decrypt',
114
+ 'kms:ReEncrypt*',
115
+ 'kms:GenerateDataKey*',
116
+ 'kms:DescribeKey',
117
+ ],
118
+ resources: ['*'],
119
+ conditions: {
120
+ ArnLike: {
121
+ 'kms:EncryptionContext:aws:logs:arn': `arn:aws:logs:${stack.region}:${stack.account}:log-group:*`,
122
+ },
123
+ },
124
+ }),
125
+ );
126
+
127
+ const accessLogs = new LogGroup(this, 'AccessLogs', {
128
+ retention: RetentionDays.ONE_YEAR,
86
129
  encryptionKey: websiteKey,
87
- objectOwnership: ObjectOwnership.OBJECT_WRITER,
88
- publicReadAccess: false,
89
- blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
130
+ removalPolicy: RemovalPolicy.DESTROY,
90
131
  });
91
- suppressRules(
92
- accessLogsBucket,
93
- ['CKV_AWS_21'],
94
- 'Access log bucket does not need versioning enabled',
95
- );
96
- suppressRules(
97
- accessLogsBucket,
98
- ['CKV_AWS_18'],
99
- 'Access log bucket does not need an access log bucket',
100
- );
101
132
 
102
133
  // S3 Bucket to hold website files
103
134
  this.websiteBucket = new Bucket(this, 'WebsiteBucket', {
@@ -110,13 +141,23 @@ export class StaticWebsite extends Construct {
110
141
  objectOwnership: ObjectOwnership.BUCKET_OWNER_ENFORCED,
111
142
  publicReadAccess: false,
112
143
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
113
- serverAccessLogsPrefix: 'website-access-logs',
114
- serverAccessLogsBucket: accessLogsBucket,
115
144
  });
145
+ suppressRules(
146
+ this.websiteBucket,
147
+ ['CKV_AWS_18'],
148
+ 'Server access logs are delivered to CloudWatch Logs',
149
+ );
150
+ this.deliverAccessLogsToCloudWatch(
151
+ 'Website',
152
+ this.websiteBucket,
153
+ accessLogs,
154
+ );
116
155
  // Web ACL
117
156
  const wafStack = new CloudfrontWebAcl(this, 'waf');
118
157
 
119
- // Cloudfront Distribution
158
+ // Bucket holding CloudFront standard access logs. CloudFront delivers its
159
+ // own logs to S3 only, so this bucket is retained; its S3 server access
160
+ // logs are delivered to CloudWatch Logs.
120
161
  const logBucket = new Bucket(this, 'DistributionLogBucket', {
121
162
  enforceSSL: true,
122
163
  autoDeleteObjects: true,
@@ -126,14 +167,18 @@ export class StaticWebsite extends Construct {
126
167
  objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
127
168
  publicReadAccess: false,
128
169
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
129
- serverAccessLogsPrefix: 'distribution-access-logs',
130
- serverAccessLogsBucket: accessLogsBucket,
131
170
  });
132
171
  suppressRules(
133
172
  logBucket,
134
173
  ['CKV_AWS_21'],
135
174
  'Distribution log bucket does not need versioning enabled',
136
175
  );
176
+ suppressRules(
177
+ logBucket,
178
+ ['CKV_AWS_18'],
179
+ 'Server access logs are delivered to CloudWatch Logs',
180
+ );
181
+ this.deliverAccessLogsToCloudWatch('Distribution', logBucket, accessLogs);
137
182
 
138
183
  // Security headers applied to all responses.
139
184
  const responseHeadersPolicy = new ResponseHeadersPolicy(
@@ -172,6 +217,13 @@ export class StaticWebsite extends Construct {
172
217
  webAclId: wafStack.wafArn,
173
218
  enableLogging: true,
174
219
  logBucket: logBucket,
220
+ ...(certificate
221
+ ? {
222
+ certificate,
223
+ domainNames,
224
+ minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
225
+ }
226
+ : {}),
175
227
  defaultBehavior: {
176
228
  origin: S3BucketOrigin.withOriginAccessControl(this.websiteBucket),
177
229
  viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
@@ -192,12 +244,14 @@ export class StaticWebsite extends Construct {
192
244
  ],
193
245
  },
194
246
  );
195
- // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
196
- suppressRules(
197
- this.cloudFrontDistribution,
198
- ['CKV_AWS_174'],
199
- 'Cloudfront default certificate does not use TLS 1.2',
200
- );
247
+ if (!certificate) {
248
+ // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesGeneral.html
249
+ suppressRules(
250
+ this.cloudFrontDistribution,
251
+ ['CKV_AWS_174'],
252
+ 'Cloudfront default certificate does not use TLS 1.2',
253
+ );
254
+ }
201
255
 
202
256
  // Deploy Website
203
257
  this.bucketDeployment = new BucketDeployment(this, 'WebsiteDeployment', {
@@ -246,6 +300,43 @@ export class StaticWebsite extends Construct {
246
300
  value: this.websiteBucket.bucketName,
247
301
  });
248
302
  }
303
+
304
+ /**
305
+ * Delivers a bucket's S3 server access logs to a CloudWatch log group using
306
+ * CloudWatch Logs vended log delivery.
307
+ */
308
+ private deliverAccessLogsToCloudWatch(
309
+ id: string,
310
+ bucket: IBucket,
311
+ logGroup: LogGroup,
312
+ ) {
313
+ const source: CfnDeliverySource = new CfnDeliverySource(
314
+ this,
315
+ `${id}AccessLogsSource`,
316
+ {
317
+ name: Lazy.string({
318
+ produce: () => Names.uniqueResourceName(source, { maxLength: 60 }),
319
+ }),
320
+ logType: 'S3_SERVER_ACCESS_LOGS',
321
+ resourceArn: bucket.bucketArn,
322
+ },
323
+ );
324
+ const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
325
+ this,
326
+ `${id}AccessLogsDestination`,
327
+ {
328
+ name: Lazy.string({
329
+ produce: () => Names.uniqueResourceName(destination, { maxLength: 60 }),
330
+ }),
331
+ destinationResourceArn: logGroup.logGroupArn,
332
+ },
333
+ );
334
+ const delivery = new CfnDelivery(this, `${id}AccessLogsDelivery`, {
335
+ deliverySourceName: source.name,
336
+ deliveryDestinationArn: destination.attrArn,
337
+ });
338
+ delivery.addDependency(source);
339
+ }
249
340
  }
250
341
 
251
342
  export class CloudfrontWebAcl extends Stack {
@@ -23,6 +23,18 @@ variable "website_file_path" {
23
23
  type = string
24
24
  }
25
25
 
26
+ variable "custom_domain_names" {
27
+ description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."
28
+ type = list(string)
29
+ default = []
30
+ }
31
+
32
+ variable "acm_certificate_arn" {
33
+ description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."
34
+ type = string
35
+ default = null
36
+ }
37
+
26
38
  # Content-Security-Policy enforced on all responses. Restricts scripts and
27
39
  # framing to mitigate XSS and clickjacking, while permitting HTTPS/WSS calls
28
40
  # (connect-src) to AWS service endpoints such as API Gateway, Cognito and
@@ -30,6 +42,10 @@ variable "website_file_path" {
30
42
  # connect-src to your specific origins once they are known.
31
43
  locals {
32
44
  content_security_policy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
45
+
46
+ # Delivery source/destination names have a 60 character maximum. Truncate the
47
+ # website name so the longest delivery name stays within the limit.
48
+ access_logs_name_prefix = substr(lower(var.website_name), 0, 16)
33
49
  }
34
50
 
35
51
 
@@ -68,6 +84,26 @@ resource "aws_kms_key" "website_key" {
68
84
  ]
69
85
  Resource = "*"
70
86
  },
87
+ {
88
+ Sid = "AllowCloudWatchLogs"
89
+ Effect = "Allow"
90
+ Principal = {
91
+ Service = "logs.${data.aws_region.current.region}.amazonaws.com"
92
+ }
93
+ Action = [
94
+ "kms:Encrypt",
95
+ "kms:Decrypt",
96
+ "kms:ReEncrypt*",
97
+ "kms:GenerateDataKey*",
98
+ "kms:DescribeKey"
99
+ ]
100
+ Resource = "*"
101
+ Condition = {
102
+ ArnLike = {
103
+ "kms:EncryptionContext:aws:logs:arn" = "arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:*"
104
+ }
105
+ }
106
+ },
71
107
  {
72
108
  Sid = "AllowCloudFrontServicePrincipalSSE-KMS"
73
109
  Effect = "Allow"
@@ -99,18 +135,12 @@ resource "aws_kms_alias" "website_key_alias" {
99
135
  target_key_id = aws_kms_key.website_key.key_id
100
136
  }
101
137
 
102
- # Access Logs Bucket
103
- resource "aws_s3_bucket" "access_logs" {
104
- #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access logs bucket
105
- #checkov:skip=CKV_AWS_144:Cross-region replication not required for access logs
106
- #checkov:skip=CKV2_AWS_62:Event notifications not required for access logs bucket
107
- #checkov:skip=CKV_AWS_21:Versioning disabled for access logs to reduce storage costs
108
- bucket = "${lower(var.website_name)}-access-logs-${random_id.bucket_suffix.hex}"
109
- force_destroy = true
110
-
111
- tags = {
112
- Name = "${lower(var.website_name)}-access-logs"
113
- }
138
+ # CloudWatch log group receiving S3 server access logs for the website and
139
+ # distribution log buckets.
140
+ resource "aws_cloudwatch_log_group" "access_logs" {
141
+ name = "/aws/s3/${lower(var.website_name)}-access-logs-${random_id.bucket_suffix.hex}"
142
+ retention_in_days = 365
143
+ kms_key_id = aws_kms_key.website_key.arn
114
144
  }
115
145
 
116
146
  resource "random_id" "unique_suffix" {
@@ -121,63 +151,12 @@ resource "random_id" "bucket_suffix" {
121
151
  byte_length = 8
122
152
  }
123
153
 
124
- resource "aws_s3_bucket_versioning" "access_logs_versioning" {
125
- bucket = aws_s3_bucket.access_logs.id
126
- versioning_configuration {
127
- status = "Disabled"
128
- }
129
- }
130
-
131
- resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs_encryption" {
132
- bucket = aws_s3_bucket.access_logs.id
133
-
134
- rule {
135
- apply_server_side_encryption_by_default {
136
- kms_master_key_id = aws_kms_key.website_key.arn
137
- sse_algorithm = "aws:kms"
138
- }
139
- }
140
- }
141
-
142
- resource "aws_s3_bucket_public_access_block" "access_logs_pab" {
143
- bucket = aws_s3_bucket.access_logs.id
144
-
145
- block_public_acls = true
146
- block_public_policy = true
147
- ignore_public_acls = true
148
- restrict_public_buckets = true
149
- }
150
-
151
- resource "aws_s3_bucket_policy" "access_logs_ssl_policy" {
152
- bucket = aws_s3_bucket.access_logs.id
153
-
154
- policy = jsonencode({
155
- Version = "2012-10-17"
156
- Statement = [
157
- {
158
- Sid = "DenyInsecureConnections"
159
- Effect = "Deny"
160
- Principal = "*"
161
- Action = "s3:*"
162
- Resource = [
163
- aws_s3_bucket.access_logs.arn,
164
- "${aws_s3_bucket.access_logs.arn}/*"
165
- ]
166
- Condition = {
167
- Bool = {
168
- "aws:SecureTransport" = "false"
169
- }
170
- }
171
- }
172
- ]
173
- })
174
- }
175
-
176
154
  # Website Bucket
177
155
  resource "aws_s3_bucket" "website" {
178
156
  #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for static website content
179
157
  #checkov:skip=CKV_AWS_144:Cross-region replication not required for static website
180
158
  #checkov:skip=CKV2_AWS_62:Event notifications not required for static website bucket
159
+ #checkov:skip=CKV_AWS_18:Server access logs are delivered to CloudWatch Logs (see aws_cloudwatch_log_delivery.website)
181
160
  bucket = "${lower(var.website_name)}-website-${random_id.bucket_suffix.hex}"
182
161
  force_destroy = true
183
162
 
@@ -221,20 +200,35 @@ resource "aws_s3_bucket_ownership_controls" "website_ownership" {
221
200
  }
222
201
  }
223
202
 
224
- resource "aws_s3_bucket_logging" "website_logging" {
225
- bucket = aws_s3_bucket.website.id
203
+ # Deliver the website bucket's server access logs to CloudWatch Logs.
204
+ resource "aws_cloudwatch_log_delivery_source" "website" {
205
+ name = "${local.access_logs_name_prefix}-website-logs-${random_id.bucket_suffix.hex}"
206
+ log_type = "S3_SERVER_ACCESS_LOGS"
207
+ resource_arn = aws_s3_bucket.website.arn
208
+ }
226
209
 
227
- target_bucket = aws_s3_bucket.access_logs.id
228
- target_prefix = "website-access-logs/"
210
+ resource "aws_cloudwatch_log_delivery_destination" "access_logs" {
211
+ name = "${local.access_logs_name_prefix}-access-logs-${random_id.bucket_suffix.hex}"
212
+
213
+ delivery_destination_configuration {
214
+ destination_resource_arn = aws_cloudwatch_log_group.access_logs.arn
215
+ }
216
+ }
217
+
218
+ resource "aws_cloudwatch_log_delivery" "website" {
219
+ delivery_source_name = aws_cloudwatch_log_delivery_source.website.name
220
+ delivery_destination_arn = aws_cloudwatch_log_delivery_destination.access_logs.arn
229
221
  }
230
222
 
231
223
 
232
- # Distribution Log Bucket
224
+ # Distribution Log Bucket — holds CloudFront standard access logs (CloudFront
225
+ # delivers only to S3). Its own S3 server access logs go to CloudWatch Logs.
233
226
  resource "aws_s3_bucket" "distribution_logs" {
234
227
  #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for CloudFront logs
235
228
  #checkov:skip=CKV_AWS_144:Cross-region replication not required for CloudFront logs
236
229
  #checkov:skip=CKV2_AWS_62:Event notifications not required for CloudFront logs bucket
237
230
  #checkov:skip=CKV_AWS_21:Versioning not required for CloudFront access logs
231
+ #checkov:skip=CKV_AWS_18:Server access logs are delivered to CloudWatch Logs (see aws_cloudwatch_log_delivery.distribution_logs)
238
232
  bucket = "${lower(var.website_name)}-distribution-logs-${random_id.bucket_suffix.hex}"
239
233
  force_destroy = true
240
234
 
@@ -280,11 +274,16 @@ resource "aws_s3_bucket_acl" "distribution_logs_acl" {
280
274
  depends_on = [aws_s3_bucket_ownership_controls.distribution_logs_ownership]
281
275
  }
282
276
 
283
- resource "aws_s3_bucket_logging" "distribution_logs_logging" {
284
- bucket = aws_s3_bucket.distribution_logs.id
277
+ # Deliver the distribution log bucket's server access logs to CloudWatch Logs.
278
+ resource "aws_cloudwatch_log_delivery_source" "distribution_logs" {
279
+ name = "${local.access_logs_name_prefix}-distribution-logs-${random_id.bucket_suffix.hex}"
280
+ log_type = "S3_SERVER_ACCESS_LOGS"
281
+ resource_arn = aws_s3_bucket.distribution_logs.arn
282
+ }
285
283
 
286
- target_bucket = aws_s3_bucket.access_logs.id
287
- target_prefix = "distribution-access-logs/"
284
+ resource "aws_cloudwatch_log_delivery" "distribution_logs" {
285
+ delivery_source_name = aws_cloudwatch_log_delivery_source.distribution_logs.name
286
+ delivery_destination_arn = aws_cloudwatch_log_delivery_destination.access_logs.arn
288
287
  }
289
288
 
290
289
  resource "aws_s3_bucket_policy" "distribution_logs_ssl_policy" {
@@ -439,6 +438,7 @@ resource "aws_cloudfront_distribution" "website" {
439
438
  is_ipv6_enabled = true
440
439
  default_root_object = "index.html"
441
440
  web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn
441
+ aliases = var.custom_domain_names
442
442
 
443
443
  logging_config {
444
444
  include_cookies = false
@@ -485,7 +485,10 @@ resource "aws_cloudfront_distribution" "website" {
485
485
  }
486
486
 
487
487
  viewer_certificate {
488
- cloudfront_default_certificate = true
488
+ cloudfront_default_certificate = var.acm_certificate_arn == null
489
+ acm_certificate_arn = var.acm_certificate_arn
490
+ ssl_support_method = var.acm_certificate_arn == null ? null : "sni-only"
491
+ minimum_protocol_version = var.acm_certificate_arn == null ? "TLSv1" : "TLSv1.2_2021"
489
492
  }
490
493
 
491
494
  tags = {