Repository navigation
Expand file tree
/
Copy pathcloudfront.tf
More file actions
339 lines (290 loc) · 10.4 KB
/
Copy pathcloudfront.tf
File metadata and controls
339 lines (290 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# The key that signs the photo cookies. An account holds at most ten CloudFront
# public keys, and the limit is not raised, so a preview (<name>-pr<N>) does
# not make one: it signs with the key of the main site, whose ids the main site
# publishes in /<name>/cloudfront-key-ids. The main site has to be deployed with
# that parameter before a preview. media_key_from picks another stack, or ""
# for a key of its own.
resource "tls_private_key" "media" {
count = local.own_media_key ? 1 : 0
algorithm = "RSA"
rsa_bits = 2048
}
resource "aws_cloudfront_public_key" "media" {
count = local.own_media_key ? 1 : 0
provider = aws.global
name = "${local.name}-media"
encoded_key = tls_private_key.media[0].public_key_pem
}
resource "aws_cloudfront_key_group" "media" {
count = local.own_media_key ? 1 : 0
provider = aws.global
name = "${local.name}-media"
items = [aws_cloudfront_public_key.media[0].id]
}
resource "aws_ssm_parameter" "media_signing_key" {
#checkov:skip=CKV_AWS_337:The AWS managed SSM key is enough for this CloudFront signing key.
count = local.own_media_key ? 1 : 0
name = "/${local.name}/cloudfront-private-key"
description = "Private key used to sign CloudFront cookies for inventory photos."
type = "SecureString"
value = tls_private_key.media[0].private_key_pem
}
# The ids a preview needs to sign with this key; the private key stays in its
# own SecureString parameter.
resource "aws_ssm_parameter" "media_key" {
#checkov:skip=CKV2_AWS_34:Ids and a parameter name, nothing secret.
count = local.own_media_key ? 1 : 0
name = "/${local.name}/cloudfront-key-ids"
description = "CloudFront key group and public key of the photo cookies, for previews."
type = "String"
value = jsonencode({
key_group_id = aws_cloudfront_key_group.media[0].id
public_key_id = aws_cloudfront_public_key.media[0].id
private_key_parameter = aws_ssm_parameter.media_signing_key[0].name
})
}
# The ids as JSON, not the private key next to them.
data "aws_ssm_parameter" "shared_media_key" {
count = local.own_media_key ? 0 : 1
name = "/${local.media_key_from}/cloudfront-key-ids"
}
# The key existed before it became optional.
moved {
from = tls_private_key.media
to = tls_private_key.media[0]
}
moved {
from = aws_cloudfront_public_key.media
to = aws_cloudfront_public_key.media[0]
}
moved {
from = aws_cloudfront_key_group.media
to = aws_cloudfront_key_group.media[0]
}
moved {
from = aws_ssm_parameter.media_signing_key
to = aws_ssm_parameter.media_signing_key[0]
}
resource "aws_cloudfront_function" "api_host" {
provider = aws.global
name = "${local.name}-api-host"
runtime = "cloudfront-js-2.0"
publish = true
comment = "Remember the viewer host so photo cookies match the site address."
code = file("${path.module}/cloudfront/api_host.js")
}
resource "aws_cloudfront_function" "media_path" {
provider = aws.global
name = "${local.name}-media-path"
runtime = "cloudfront-js-2.0"
publish = true
comment = "Strip /media before reading the private photo bucket."
code = file("${path.module}/cloudfront/media.js")
}
resource "aws_cloudfront_cache_policy" "media" {
provider = aws.global
name = "${local.name}-media"
comment = "Cache photos by path. The signed cookie is checked on every request and is not part of the cache key."
default_ttl = 86400
max_ttl = 31536000
min_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
headers_config {
header_behavior = "none"
}
cookies_config {
cookie_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
}
}
resource "aws_cloudfront_response_headers_policy" "media" {
provider = aws.global
name = "${local.name}-media"
security_headers_config {
strict_transport_security {
access_control_max_age_sec = 31536000
include_subdomains = true
override = true
preload = true
}
}
custom_headers_config {
items {
header = "Cache-Control"
override = true
value = "private, max-age=86400"
}
}
}
resource "aws_cloudfront_origin_access_control" "web" {
provider = aws.global
name = local.web_bucket
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_cache_policy" "web" {
provider = aws.global
name = "${local.name}-web"
comment = "Short cache for the inventory shell. API responses are not cached."
default_ttl = 0
max_ttl = 86400
min_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
headers_config {
header_behavior = "none"
}
cookies_config {
cookie_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
}
}
resource "aws_cloudfront_response_headers_policy" "web" {
provider = aws.global
name = "${local.name}-web"
security_headers_config {
content_type_options {
override = true
}
frame_options {
frame_option = "DENY"
override = true
}
strict_transport_security {
access_control_max_age_sec = 31536000
include_subdomains = true
override = true
preload = true
}
referrer_policy {
referrer_policy = "no-referrer"
override = true
}
content_security_policy {
content_security_policy = local.csp
override = true
}
}
}
data "aws_cloudfront_cache_policy" "caching_disabled" {
provider = aws.global
name = "Managed-CachingDisabled"
}
data "aws_cloudfront_origin_request_policy" "all_viewer_except_host" {
provider = aws.global
name = "Managed-AllViewerExceptHostHeader"
}
resource "aws_cloudfront_function" "routes" {
provider = aws.global
name = "${local.name}-routes"
runtime = "cloudfront-js-2.0"
publish = true
comment = "Serve the inventory shell for direct card links."
code = file("${path.module}/cloudfront/routes.js")
}
resource "aws_cloudfront_distribution" "web" {
#checkov:skip=CKV_AWS_374: "Ensure AWS CloudFront web distribution has geo restriction enabled"
#checkov:skip=CKV_AWS_68:WAFv2 costs about the whole monthly budget of this account.
#checkov:skip=CKV2_AWS_47:No WAFv2 WebACL is attached.
#checkov:skip=CKV_AWS_310:One S3 origin and one API origin are enough.
#checkov:skip=CKV_AWS_86:The access logs are standard logging v2 (access-logs.tf); the legacy logging block needs a bucket ACL.
#checkov:skip=CKV_AWS_174:The default CloudFront certificate does not allow choosing the TLS policy.
#checkov:skip=CKV2_AWS_42:The demo runs on the *.cloudfront.net address with its default certificate, without a domain of its own.
#checkov:skip=CKV_AWS_305:Geo restriction is not required for this private app.
provider = aws.global
enabled = true
is_ipv6_enabled = true
comment = local.name
price_class = "PriceClass_100"
default_root_object = "index.html"
http_version = "http2and3"
wait_for_deployment = true
origin {
domain_name = aws_s3_bucket.web.bucket_regional_domain_name
origin_id = "web"
origin_access_control_id = aws_cloudfront_origin_access_control.web.id
connection_attempts = 3
connection_timeout = 10
}
origin {
domain_name = aws_s3_bucket.media.bucket_regional_domain_name
origin_id = "media"
origin_access_control_id = aws_cloudfront_origin_access_control.web.id
connection_attempts = 3
connection_timeout = 10
}
origin {
domain_name = "${aws_apigatewayv2_api.http.id}.execute-api.${var.region}.amazonaws.com"
origin_id = "api"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
origin_read_timeout = 60
origin_keepalive_timeout = 5
}
}
default_cache_behavior {
target_origin_id = "web"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.web.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.web.id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.routes.arn
}
}
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = "api"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host.id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.api_host.arn
}
}
ordered_cache_behavior {
path_pattern = "/media/*"
target_origin_id = "media"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.media.id
trusted_key_groups = [local.media_key.key_group_id]
response_headers_policy_id = aws_cloudfront_response_headers_policy.media.id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.media_path.arn
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
# The demo lives on the CloudFront address, so the default certificate is
# used. A custom domain would add an ACM certificate and an alias here.
viewer_certificate {
cloudfront_default_certificate = true
}
}