From d378e4b3789158c87ce1e5c6f403add82b0100b1 Mon Sep 17 00:00:00 2001 From: John McGehee Date: Mon, 26 Apr 2021 16:26:21 -0700 Subject: [PATCH] Make S3 website private; add S3 origin (#105) Co-authored-by: cloudpossebot <11232728+cloudpossebot@users.noreply.github.com> Co-authored-by: John McGehee Co-authored-by: nitro Co-authored-by: Philippe M. Chiasson --- README.md | 31 +++++++++++++++++++++++++------ README.yaml | 25 +++++++++++++++++++++---- docs/terraform.md | 8 ++++++-- main.tf | 28 +++++++++++++++++++++++++--- variables.tf | 29 +++++++++++++++++++++++++++-- versions.tf | 4 ++++ 6 files changed, 108 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5c2f1589..f1c45ad5 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,21 @@ module "cdn" { } ``` +### Using an S3 Static Website Origin + +When variable `website_enabled` is set to `true`, the S3 origin is configured +as a static website. The S3 static website has the advantage of redirecting +URL `subdir/` to `subdir/index.html` without requiring a +[Lambda@Edge function to perform the redirection](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/). +The S3 static website responds only to CloudFront, preventing direct access to +S3. + +In addition to setting `website_enabled=true`, you must also: + +* Specify at least one `aliases`, like `["example.com"]` or + `["example.com", "www.example.com"]` +* Specify an ACM certificate + ### Generating ACM Certificate ```hcl @@ -176,8 +191,6 @@ Or use the AWS cli to [request new ACM certifiates](http://docs.aws.amazon.com/a aws acm request-certificate --domain-name example.com --subject-alternative-names a.example.com b.example.com *.c.example.com ``` - - __NOTE__: Although AWS Certificate Manager is supported in many AWS regions, to use an SSL certificate with CloudFront, it should be requested only in US East (N. Virginia) region. @@ -218,12 +231,14 @@ Available targets: |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 2.0 | +| [random](#requirement\_random) | >= 2.2 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 2.0 | +| [random](#provider\_random) | >= 2.2 | ## Modules @@ -243,6 +258,7 @@ Available targets: | [aws_s3_bucket.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | | [aws_s3_bucket_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | | [aws_s3_bucket_public_access_block.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [random_password.referer](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [aws_iam_policy_document.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.origin_website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_s3_bucket.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket) | data source | @@ -272,7 +288,7 @@ Available targets: | [cors\_max\_age\_seconds](#input\_cors\_max\_age\_seconds) | Time in seconds that browser can cache the response for S3 bucket | `number` | `3600` | no | | [custom\_error\_response](#input\_custom\_error\_response) | List of one or more custom error response element maps |
list(object({
error_caching_min_ttl = string
error_code = string
response_code = string
response_page_path = string
}))
| `[]` | no | | [custom\_origin\_headers](#input\_custom\_origin\_headers) | A list of origin header parameters that will be sent to origin | `list(object({ name = string, value = string }))` | `[]` | no | -| [custom\_origins](#input\_custom\_origins) | One or more custom origins for this distribution (multiples allowed). See documentation for configuration options description https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | +| [custom\_origins](#input\_custom\_origins) | A list of custom origins (such as apps or S3 websites) for this distribution.
See the Terraform documentation for configuration options
https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | | [default\_root\_object](#input\_default\_root\_object) | Object that CloudFront return when requests the root URL | `string` | `"index.html"` | no | | [default\_ttl](#input\_default\_ttl) | Default amount of time (in seconds) that an object is in a CloudFront cache | `number` | `60` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | @@ -321,6 +337,7 @@ Available targets: | [redirect\_all\_requests\_to](#input\_redirect\_all\_requests\_to) | A hostname to redirect all website requests for this distribution to. If this is set, it overrides other website settings | `string` | `""` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [routing\_rules](#input\_routing\_rules) | A json array containing routing rules describing redirect behavior and when redirects are applied | `string` | `""` | no | +| [s3\_origins](#input\_s3\_origins) | A list of S3 origins for this distribution. S3 buckets configured as websites
are custom\_origins, not s3\_origins. See the Terraform documentation for
configuration options
https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
s3_origin_config = object({
origin_access_identity = string
})
}))
| `[]` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | | [trusted\_signers](#input\_trusted\_signers) | The AWS accounts, if any, that you want to allow to create signed URLs for private content. 'self' is acceptable. | `list(string)` | `[]` | no | @@ -328,7 +345,7 @@ Available targets: | [viewer\_protocol\_policy](#input\_viewer\_protocol\_policy) | allow-all, redirect-to-https | `string` | `"redirect-to-https"` | no | | [wait\_for\_deployment](#input\_wait\_for\_deployment) | When set to 'true' the resource will wait for the distribution status to change from InProgress to Deployed | `bool` | `true` | no | | [web\_acl\_id](#input\_web\_acl\_id) | ID of the AWS WAF web ACL that is associated with the distribution | `string` | `""` | no | -| [website\_enabled](#input\_website\_enabled) | Set to true to use an S3 static website as origin | `bool` | `false` | no | +| [website\_enabled](#input\_website\_enabled) | Set to true to use an S3 static website as origin. If you set this to true, see
README for more important instructions. | `bool` | `false` | no | ## Outputs @@ -496,8 +513,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply ### Contributors -| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Jamie Nelson][Jamie-BitFlight_avatar]][Jamie-BitFlight_homepage]
[Jamie Nelson][Jamie-BitFlight_homepage] | [![Clive Zagno][cliveza_avatar]][cliveza_homepage]
[Clive Zagno][cliveza_homepage] | [![David Mattia][dmattia_avatar]][dmattia_homepage]
[David Mattia][dmattia_homepage] | [![RB][nitrocode_avatar]][nitrocode_homepage]
[RB][nitrocode_homepage] | -|---|---|---|---|---|---| +| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Jamie Nelson][Jamie-BitFlight_avatar]][Jamie-BitFlight_homepage]
[Jamie Nelson][Jamie-BitFlight_homepage] | [![Clive Zagno][cliveza_avatar]][cliveza_homepage]
[Clive Zagno][cliveza_homepage] | [![David Mattia][dmattia_avatar]][dmattia_homepage]
[David Mattia][dmattia_homepage] | [![RB][nitrocode_avatar]][nitrocode_homepage]
[RB][nitrocode_homepage] | [![John McGehee][jmcgeheeiv_avatar]][jmcgeheeiv_homepage]
[John McGehee][jmcgeheeiv_homepage] | +|---|---|---|---|---|---|---| [osterman_homepage]: https://github.com/osterman @@ -512,6 +529,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [dmattia_avatar]: https://img.cloudposse.com/150x150/https://github.com/dmattia.png [nitrocode_homepage]: https://github.com/nitrocode [nitrocode_avatar]: https://img.cloudposse.com/150x150/https://github.com/nitrocode.png + [jmcgeheeiv_homepage]: https://github.com/jmcgeheeiv + [jmcgeheeiv_avatar]: https://img.cloudposse.com/150x150/https://github.com/jmcgeheeiv.png [![README Footer][readme_footer_img]][readme_footer_link] [![Beacon][beacon]][website] diff --git a/README.yaml b/README.yaml index 1697634a..902fe27a 100644 --- a/README.yaml +++ b/README.yaml @@ -1,8 +1,10 @@ --- # # This is the canonical configuration for the `README.md` -# Run `make readme` to rebuild the `README.md` -# +# To rebuild `README.md`: +# 1) Make all changes to `README.yaml` +# 2) Run `make init` (you only need to do this once) +# 3) Run`make readme` to rebuild this file. # Name of this project name: terraform-aws-cloudfront-s3-cdn @@ -93,6 +95,21 @@ usage: |- } ``` + ### Using an S3 Static Website Origin + + When variable `website_enabled` is set to `true`, the S3 origin is configured + as a static website. The S3 static website has the advantage of redirecting + URL `subdir/` to `subdir/index.html` without requiring a + [Lambda@Edge function to perform the redirection](https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/). + The S3 static website responds only to CloudFront, preventing direct access to + S3. + + In addition to setting `website_enabled=true`, you must also: + + * Specify at least one `aliases`, like `["example.com"]` or + `["example.com", "www.example.com"]` + * Specify an ACM certificate + ### Generating ACM Certificate ```hcl @@ -139,8 +156,6 @@ usage: |- aws acm request-certificate --domain-name example.com --subject-alternative-names a.example.com b.example.com *.c.example.com ``` - - __NOTE__: Although AWS Certificate Manager is supported in many AWS regions, to use an SSL certificate with CloudFront, it should be requested only in US East (N. Virginia) region. @@ -175,3 +190,5 @@ contributors: github: "dmattia" - name: "RB" github: "nitrocode" + - name: "John McGehee" + github: "jmcgeheeiv" diff --git a/docs/terraform.md b/docs/terraform.md index 802abe84..3bf13995 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -5,12 +5,14 @@ |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 2.0 | +| [random](#requirement\_random) | >= 2.2 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 2.0 | +| [random](#provider\_random) | >= 2.2 | ## Modules @@ -30,6 +32,7 @@ | [aws_s3_bucket.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | | [aws_s3_bucket_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | | [aws_s3_bucket_public_access_block.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [random_password.referer](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [aws_iam_policy_document.origin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.origin_website](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_s3_bucket.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket) | data source | @@ -59,7 +62,7 @@ | [cors\_max\_age\_seconds](#input\_cors\_max\_age\_seconds) | Time in seconds that browser can cache the response for S3 bucket | `number` | `3600` | no | | [custom\_error\_response](#input\_custom\_error\_response) | List of one or more custom error response element maps |
list(object({
error_caching_min_ttl = string
error_code = string
response_code = string
response_page_path = string
}))
| `[]` | no | | [custom\_origin\_headers](#input\_custom\_origin\_headers) | A list of origin header parameters that will be sent to origin | `list(object({ name = string, value = string }))` | `[]` | no | -| [custom\_origins](#input\_custom\_origins) | One or more custom origins for this distribution (multiples allowed). See documentation for configuration options description https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | +| [custom\_origins](#input\_custom\_origins) | A list of custom origins (such as apps or S3 websites) for this distribution.
See the Terraform documentation for configuration options
https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | | [default\_root\_object](#input\_default\_root\_object) | Object that CloudFront return when requests the root URL | `string` | `"index.html"` | no | | [default\_ttl](#input\_default\_ttl) | Default amount of time (in seconds) that an object is in a CloudFront cache | `number` | `60` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | @@ -108,6 +111,7 @@ | [redirect\_all\_requests\_to](#input\_redirect\_all\_requests\_to) | A hostname to redirect all website requests for this distribution to. If this is set, it overrides other website settings | `string` | `""` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [routing\_rules](#input\_routing\_rules) | A json array containing routing rules describing redirect behavior and when redirects are applied | `string` | `""` | no | +| [s3\_origins](#input\_s3\_origins) | A list of S3 origins for this distribution. S3 buckets configured as websites
are custom\_origins, not s3\_origins. See the Terraform documentation for
configuration options
https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments |
list(object({
domain_name = string
origin_id = string
origin_path = string
s3_origin_config = object({
origin_access_identity = string
})
}))
| `[]` | no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | | [trusted\_signers](#input\_trusted\_signers) | The AWS accounts, if any, that you want to allow to create signed URLs for private content. 'self' is acceptable. | `list(string)` | `[]` | no | @@ -115,7 +119,7 @@ | [viewer\_protocol\_policy](#input\_viewer\_protocol\_policy) | allow-all, redirect-to-https | `string` | `"redirect-to-https"` | no | | [wait\_for\_deployment](#input\_wait\_for\_deployment) | When set to 'true' the resource will wait for the distribution status to change from InProgress to Deployed | `bool` | `true` | no | | [web\_acl\_id](#input\_web\_acl\_id) | ID of the AWS WAF web ACL that is associated with the distribution | `string` | `""` | no | -| [website\_enabled](#input\_website\_enabled) | Set to true to use an S3 static website as origin | `bool` | `false` | no | +| [website\_enabled](#input\_website\_enabled) | Set to true to use an S3 static website as origin. If you set this to true, see
README for more important instructions. | `bool` | `false` | no | ## Outputs diff --git a/main.tf b/main.tf index 304757ee..fe4884b4 100644 --- a/main.tf +++ b/main.tf @@ -28,6 +28,11 @@ resource "aws_cloudfront_origin_access_identity" "default" { comment = module.this.id } +resource "random_password" "referer" { + length = 32 + special = false +} + data "aws_iam_policy_document" "origin" { count = module.this.enabled ? 1 : 0 @@ -73,6 +78,11 @@ data "aws_iam_policy_document" "origin_website" { type = "AWS" identifiers = ["*"] } + condition { + test = "StringEquals" + variable = "aws:referer" + values = [random_password.referer.result] + } } } @@ -235,7 +245,7 @@ resource "aws_cloudfront_distribution" "default" { } } dynamic "custom_header" { - for_each = var.custom_origin_headers + for_each = var.website_enabled ? concat([{ name = "referer", value = random_password.referer.result }], var.custom_origin_headers) : var.custom_origin_headers content { name = custom_header.value["name"] value = custom_header.value["value"] @@ -257,8 +267,8 @@ resource "aws_cloudfront_distribution" "default" { } } custom_origin_config { - http_port = lookup(origin.value.custom_origin_config, "http_port", null) - https_port = lookup(origin.value.custom_origin_config, "https_port", null) + http_port = lookup(origin.value.custom_origin_config, "http_port", 80) + https_port = lookup(origin.value.custom_origin_config, "https_port", 443) origin_protocol_policy = lookup(origin.value.custom_origin_config, "origin_protocol_policy", "https-only") origin_ssl_protocols = lookup(origin.value.custom_origin_config, "origin_ssl_protocols", ["TLSv1.2"]) origin_keepalive_timeout = lookup(origin.value.custom_origin_config, "origin_keepalive_timeout", 60) @@ -267,6 +277,18 @@ resource "aws_cloudfront_distribution" "default" { } } + dynamic "origin" { + for_each = var.s3_origins + content { + domain_name = origin.value.domain_name + origin_id = origin.value.origin_id + origin_path = lookup(origin.value, "origin_path", "") + s3_origin_config { + origin_access_identity = lookup(origin.value.s3_origin_config, "origin_access_identity", "") + } + } + } + viewer_certificate { acm_certificate_arn = var.acm_certificate_arn ssl_support_method = var.acm_certificate_arn == "" ? "" : "sni-only" diff --git a/variables.tf b/variables.tf index 76296d91..3e669954 100644 --- a/variables.tf +++ b/variables.tf @@ -380,13 +380,38 @@ variable "custom_origins" { }) })) default = [] - description = "One or more custom origins for this distribution (multiples allowed). See documentation for configuration options description https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments" + description = <