From f06304f0bd4124f28edb651b157efd320a260a63 Mon Sep 17 00:00:00 2001 From: joshAtRula Date: Fri, 24 Oct 2025 10:46:59 -0400 Subject: [PATCH] Add cloudfront response header policy support --- README.md | 6 ++ examples/complete/main.tf | 75 +++++++++++++++++++ main.tf | 147 +++++++++++++++++++++++++++++++++++++- outputs.tf | 15 ++++ variables.tf | 82 +++++++++++++++++++++ wrappers/main.tf | 2 + 6 files changed, 324 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b644ea3..34bfd55 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ No modules. | [aws_cloudfront_monitoring_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_monitoring_subscription) | resource | | [aws_cloudfront_origin_access_control.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource | | [aws_cloudfront_origin_access_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_identity) | resource | +| [aws_cloudfront_response_headers_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_response_headers_policy) | resource | | [aws_cloudfront_vpc_origin.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_vpc_origin) | resource | | [aws_cloudfront_cache_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source | | [aws_cloudfront_origin_request_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_origin_request_policy) | data source | @@ -143,6 +144,7 @@ No modules. | [create\_monitoring\_subscription](#input\_create\_monitoring\_subscription) | If enabled, the resource for monitoring subscription will created. | `bool` | `false` | no | | [create\_origin\_access\_control](#input\_create\_origin\_access\_control) | Controls if CloudFront origin access control should be created | `bool` | `false` | no | | [create\_origin\_access\_identity](#input\_create\_origin\_access\_identity) | Controls if CloudFront origin access identity should be created | `bool` | `false` | no | +| [create\_response\_headers\_policy](#input\_create\_response\_headers\_policy) | Controls if CloudFront response headers policies should be created | `bool` | `false` | no | | [create\_vpc\_origin](#input\_create\_vpc\_origin) | If enabled, the resource for VPC origin will be created. | `bool` | `false` | no | | [custom\_error\_response](#input\_custom\_error\_response) | One or more custom error response elements | `any` | `{}` | no | | [default\_cache\_behavior](#input\_default\_cache\_behavior) | The default cache behavior for this distribution | `any` | `null` | no | @@ -159,6 +161,7 @@ No modules. | [origin\_group](#input\_origin\_group) | One or more origin\_group for this distribution (multiples allowed). | `any` | `{}` | no | | [price\_class](#input\_price\_class) | The price class for this distribution. One of PriceClass\_All, PriceClass\_200, PriceClass\_100 | `string` | `null` | no | | [realtime\_metrics\_subscription\_status](#input\_realtime\_metrics\_subscription\_status) | A flag that indicates whether additional CloudWatch metrics are enabled for a given CloudFront distribution. Valid values are `Enabled` and `Disabled`. | `string` | `"Enabled"` | no | +| [response\_headers\_policy](#input\_response\_headers\_policy) | Map of CloudFront response headers policies with their configurations |
map(object({
name = optional(string)
comment = optional(string)

cors_config = optional(object({
access_control_allow_credentials = bool
origin_override = bool
access_control_allow_headers = object({
items = list(string)
})
access_control_allow_methods = object({
items = list(string)
})
access_control_allow_origins = object({
items = list(string)
})
access_control_expose_headers = optional(object({
items = list(string)
}))
access_control_max_age_sec = optional(number)
}))

custom_headers_config = optional(object({
items = list(object({
header = string
override = bool
value = string
}))
}))

remove_headers_config = optional(object({
items = list(object({
header = string
}))
}))

security_headers_config = optional(object({
content_security_policy = optional(object({
content_security_policy = string
override = bool
}))
content_type_options = optional(object({
override = bool
}))
frame_options = optional(object({
frame_option = string
override = bool
}))
referrer_policy = optional(object({
referrer_policy = string
override = bool
}))
strict_transport_security = optional(object({
access_control_max_age_sec = number
override = bool
include_subdomains = optional(bool)
preload = optional(bool)
}))
xss_protection = optional(object({
mode_block = bool
override = bool
protection = bool
report_uri = optional(string)
}))
}))

server_timing_headers_config = optional(object({
enabled = bool
sampling_rate = number
}))
}))
| `{}` | no | | [retain\_on\_delete](#input\_retain\_on\_delete) | Disables the distribution instead of deleting it when destroying the resource through Terraform. If this is set, the distribution needs to be deleted manually afterwards. | `bool` | `false` | no | | [staging](#input\_staging) | Whether the distribution is a staging distribution. | `bool` | `false` | no | | [tags](#input\_tags) | A map of tags to assign to the resource. | `map(string)` | `null` | no | @@ -189,6 +192,9 @@ No modules. | [cloudfront\_origin\_access\_identities](#output\_cloudfront\_origin\_access\_identities) | The origin access identities created | | [cloudfront\_origin\_access\_identity\_iam\_arns](#output\_cloudfront\_origin\_access\_identity\_iam\_arns) | The IAM arns of the origin access identities created | | [cloudfront\_origin\_access\_identity\_ids](#output\_cloudfront\_origin\_access\_identity\_ids) | The IDS of the origin access identities created | +| [cloudfront\_response\_headers\_policies](#output\_cloudfront\_response\_headers\_policies) | The response headers policies created | +| [cloudfront\_response\_headers\_policy\_etags](#output\_cloudfront\_response\_headers\_policy\_etags) | The ETags of the response headers policies created | +| [cloudfront\_response\_headers\_policy\_ids](#output\_cloudfront\_response\_headers\_policy\_ids) | The IDs of the response headers policies created | | [cloudfront\_vpc\_origin\_ids](#output\_cloudfront\_vpc\_origin\_ids) | The IDS of the VPC origin created | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 76bc588..2a24ced 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -176,6 +176,8 @@ module "cloudfront" { cache_policy_name = "Managed-CachingOptimized" origin_request_policy_name = "Managed-UserAgentRefererHeaders" response_headers_policy_name = "Managed-SimpleCORS" + # using a response header policy you're dynamically creating below + # response_header_policy: "cors_policy" function_association = { # Valid keys: viewer-request, viewer-response @@ -231,6 +233,79 @@ module "cloudfront" { locations = ["NO", "UA", "US", "GB"] } + create_response_headers_policy = true + response_headers_policy = { + cors_policy = { + name = "CORSPolicy" + comment = "CORS configuration for API" + + cors_config = { + access_control_allow_credentials = true + origin_override = true + + access_control_allow_headers = { + items = ["*"] + } + + access_control_allow_methods = { + items = ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + } + + access_control_allow_origins = { + items = ["https://example.com", "https://app.example.com"] + } + + access_control_expose_headers = { + items = ["X-Custom-Header", "X-Request-Id"] + } + + access_control_max_age_sec = 3600 + } + } + custom_headers = { + name = "CustomHeadersPolicy" + comment = "Add custom response headers" + + custom_headers_config = { + items = [ + { + header = "X-Powered-By" + override = true + value = "MyApp/1.0" + }, + { + header = "X-API-Version" + override = false + value = "v2" + }, + { + header = "Cache-Control" + override = true + value = "public, max-age=3600" + } + ] + } + } + remove_headers = { + name = "RemoveHeadersPolicy" + comment = "Remove unwanted headers from origin" + + remove_headers_config = { + items = [ + { + header = "x-robots-tag" + }, + { + header = "server" + }, + { + header = "x-powered-by" + } + ] + } + } + } + } ###### diff --git a/main.tf b/main.tf index f773d7e..9482281 100644 --- a/main.tf +++ b/main.tf @@ -1,9 +1,150 @@ locals { - create_origin_access_identity = var.create_origin_access_identity && length(keys(var.origin_access_identities)) > 0 - create_origin_access_control = var.create_origin_access_control && length(keys(var.origin_access_control)) > 0 - create_vpc_origin = var.create_vpc_origin && length(keys(var.vpc_origin)) > 0 + create_origin_access_identity = var.create_origin_access_identity && length(keys(var.origin_access_identities)) > 0 + create_origin_access_control = var.create_origin_access_control && length(keys(var.origin_access_control)) > 0 + create_vpc_origin = var.create_vpc_origin && length(keys(var.vpc_origin)) > 0 + create_response_headers_policy = var.create_response_headers_policy && length(keys(var.response_headers_policy)) > 0 } +resource "aws_cloudfront_response_headers_policy" "this" { + for_each = local.create_response_headers_policy ? var.response_headers_policy : {} + + name = each.value.name != null ? each.value.name : each.key + comment = each.value.comment + + dynamic "cors_config" { + for_each = each.value.cors_config != null ? [each.value.cors_config] : [] + + content { + access_control_allow_credentials = cors_config.value.access_control_allow_credentials + origin_override = cors_config.value.origin_override + access_control_max_age_sec = cors_config.value.access_control_max_age_sec != null ? cors_config.value.access_control_max_age_sec : null + + access_control_allow_headers { + items = cors_config.value.access_control_allow_headers.items + } + + access_control_allow_methods { + items = cors_config.value.access_control_allow_methods.items + } + + access_control_allow_origins { + items = cors_config.value.access_control_allow_origins.items + } + + dynamic "access_control_expose_headers" { + for_each = cors_config.value.access_control_expose_headers != null ? [cors_config.value.access_control_expose_headers] : [] + + content { + items = access_control_expose_headers.value.items + } + } + } + } + + dynamic "custom_headers_config" { + for_each = each.value.custom_headers_config != null ? [each.value.custom_headers_config] : [] + + content { + dynamic "items" { + for_each = custom_headers_config.value.items + + content { + header = items.value.header + override = items.value.override + value = items.value.value + } + } + } + } + + dynamic "remove_headers_config" { + for_each = each.value.remove_headers_config != null ? [each.value.remove_headers_config] : [] + + content { + dynamic "items" { + for_each = remove_headers_config.value.items + + content { + header = items.value.header + } + } + } + } + + dynamic "security_headers_config" { + for_each = each.value.security_headers_config != null ? [each.value.security_headers_config] : [] + + content { + dynamic "content_security_policy" { + for_each = security_headers_config.value.content_security_policy != null ? [security_headers_config.value.content_security_policy] : [] + + content { + content_security_policy = content_security_policy.value.content_security_policy + override = content_security_policy.value.override + } + } + + dynamic "content_type_options" { + for_each = security_headers_config.value.content_type_options != null ? [security_headers_config.value.content_type_options] : [] + + content { + override = content_type_options.value.override + } + } + + dynamic "frame_options" { + for_each = security_headers_config.value.frame_options != null ? [security_headers_config.value.frame_options] : [] + + content { + frame_option = frame_options.value.frame_option + override = frame_options.value.override + } + } + + dynamic "referrer_policy" { + for_each = security_headers_config.value.referrer_policy != null ? [security_headers_config.value.referrer_policy] : [] + + content { + referrer_policy = referrer_policy.value.referrer_policy + override = referrer_policy.value.override + } + } + + dynamic "strict_transport_security" { + for_each = security_headers_config.value.strict_transport_security != null ? [security_headers_config.value.strict_transport_security] : [] + + content { + access_control_max_age_sec = strict_transport_security.value.access_control_max_age_sec + override = strict_transport_security.value.override + include_subdomains = strict_transport_security.value.include_subdomains + preload = strict_transport_security.value.preload + } + } + + dynamic "xss_protection" { + for_each = security_headers_config.value.xss_protection != null ? [security_headers_config.value.xss_protection] : [] + + content { + mode_block = xss_protection.value.mode_block + override = xss_protection.value.override + protection = xss_protection.value.protection + report_uri = xss_protection.value.report_uri + } + } + } + } + + dynamic "server_timing_headers_config" { + for_each = each.value.server_timing_headers_config != null ? [each.value.server_timing_headers_config] : [] + + content { + enabled = server_timing_headers_config.value.enabled + sampling_rate = server_timing_headers_config.value.sampling_rate + } + } +} + + resource "aws_cloudfront_origin_access_identity" "this" { for_each = local.create_origin_access_identity ? var.origin_access_identities : {} diff --git a/outputs.tf b/outputs.tf index 29e7642..c9efa9b 100644 --- a/outputs.tf +++ b/outputs.tf @@ -87,3 +87,18 @@ output "cloudfront_vpc_origin_ids" { description = "The IDS of the VPC origin created" value = local.create_vpc_origin ? [for v in aws_cloudfront_vpc_origin.this : v.id] : [] } + +output "cloudfront_response_headers_policies" { + description = "The response headers policies created" + value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v } : {} +} + +output "cloudfront_response_headers_policy_ids" { + description = "The IDs of the response headers policies created" + value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v.id } : {} +} + +output "cloudfront_response_headers_policy_etags" { + description = "The ETags of the response headers policies created" + value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v.etag } : {} +} diff --git a/variables.tf b/variables.tf index afeec33..83e987a 100644 --- a/variables.tf +++ b/variables.tf @@ -210,3 +210,85 @@ variable "vpc_origin_timeouts" { type = map(string) default = {} } + +variable "create_response_headers_policy" { + description = "Controls if CloudFront response headers policies should be created" + type = bool + default = false +} + +variable "response_headers_policy" { + description = "Map of CloudFront response headers policies with their configurations" + type = map(object({ + name = optional(string) + comment = optional(string) + + cors_config = optional(object({ + access_control_allow_credentials = bool + origin_override = bool + access_control_allow_headers = object({ + items = list(string) + }) + access_control_allow_methods = object({ + items = list(string) + }) + access_control_allow_origins = object({ + items = list(string) + }) + access_control_expose_headers = optional(object({ + items = list(string) + })) + access_control_max_age_sec = optional(number) + })) + + custom_headers_config = optional(object({ + items = list(object({ + header = string + override = bool + value = string + })) + })) + + remove_headers_config = optional(object({ + items = list(object({ + header = string + })) + })) + + security_headers_config = optional(object({ + content_security_policy = optional(object({ + content_security_policy = string + override = bool + })) + content_type_options = optional(object({ + override = bool + })) + frame_options = optional(object({ + frame_option = string + override = bool + })) + referrer_policy = optional(object({ + referrer_policy = string + override = bool + })) + strict_transport_security = optional(object({ + access_control_max_age_sec = number + override = bool + include_subdomains = optional(bool) + preload = optional(bool) + })) + xss_protection = optional(object({ + mode_block = bool + override = bool + protection = bool + report_uri = optional(string) + })) + })) + + server_timing_headers_config = optional(object({ + enabled = bool + sampling_rate = number + })) + })) + default = {} +} diff --git a/wrappers/main.tf b/wrappers/main.tf index 750d27e..1bdb676 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -10,6 +10,7 @@ module "wrapper" { create_monitoring_subscription = try(each.value.create_monitoring_subscription, var.defaults.create_monitoring_subscription, false) create_origin_access_control = try(each.value.create_origin_access_control, var.defaults.create_origin_access_control, false) create_origin_access_identity = try(each.value.create_origin_access_identity, var.defaults.create_origin_access_identity, false) + create_response_headers_policy = try(each.value.create_response_headers_policy, var.defaults.create_response_headers_policy, false) create_vpc_origin = try(each.value.create_vpc_origin, var.defaults.create_vpc_origin, false) custom_error_response = try(each.value.custom_error_response, var.defaults.custom_error_response, {}) default_cache_behavior = try(each.value.default_cache_behavior, var.defaults.default_cache_behavior, null) @@ -33,6 +34,7 @@ module "wrapper" { origin_group = try(each.value.origin_group, var.defaults.origin_group, {}) price_class = try(each.value.price_class, var.defaults.price_class, null) realtime_metrics_subscription_status = try(each.value.realtime_metrics_subscription_status, var.defaults.realtime_metrics_subscription_status, "Enabled") + response_headers_policy = try(each.value.response_headers_policy, var.defaults.response_headers_policy, {}) retain_on_delete = try(each.value.retain_on_delete, var.defaults.retain_on_delete, false) staging = try(each.value.staging, var.defaults.staging, false) tags = try(each.value.tags, var.defaults.tags, null)