From b63eb98d1b89710c42af8d032c165776712b6def Mon Sep 17 00:00:00 2001 From: James Armes Date: Wed, 17 Apr 2024 15:49:55 -0400 Subject: [PATCH] Added CloudFront WAF module. --- aws/cloudfront_waf/README.md | 3 + aws/cloudfront_waf/data.tf | 11 ++ aws/cloudfront_waf/dns.tf | 46 +++++++ aws/cloudfront_waf/locals.tf | 5 + aws/cloudfront_waf/main.tf | 213 ++++++++++++++++++++++++++++++++ aws/cloudfront_waf/variables.tf | 32 +++++ aws/cloudfront_waf/versions.tf | 10 ++ 7 files changed, 320 insertions(+) create mode 100644 aws/cloudfront_waf/README.md create mode 100644 aws/cloudfront_waf/data.tf create mode 100644 aws/cloudfront_waf/dns.tf create mode 100644 aws/cloudfront_waf/locals.tf create mode 100644 aws/cloudfront_waf/main.tf create mode 100644 aws/cloudfront_waf/variables.tf create mode 100644 aws/cloudfront_waf/versions.tf diff --git a/aws/cloudfront_waf/README.md b/aws/cloudfront_waf/README.md new file mode 100644 index 0000000..a9f86f0 --- /dev/null +++ b/aws/cloudfront_waf/README.md @@ -0,0 +1,3 @@ +# CloudFront WAF Module + +# TODO: Complete this README diff --git a/aws/cloudfront_waf/data.tf b/aws/cloudfront_waf/data.tf new file mode 100644 index 0000000..3620dac --- /dev/null +++ b/aws/cloudfront_waf/data.tf @@ -0,0 +1,11 @@ +data "aws_cloudfront_origin_request_policy" "managed_cors" { + name = "Managed-CORS-CustomOrigin" +} + +data "aws_cloudfront_response_headers_policy" "managed_cors" { + name = "Managed-SimpleCORS" +} + +data "aws_route53_zone" "domain" { + name = var.domain +} diff --git a/aws/cloudfront_waf/dns.tf b/aws/cloudfront_waf/dns.tf new file mode 100644 index 0000000..34c9463 --- /dev/null +++ b/aws/cloudfront_waf/dns.tf @@ -0,0 +1,46 @@ +resource "aws_route53_record" "subdomain" { + zone_id = data.aws_route53_zone.domain.zone_id + name = "${local.subdomain}.${var.domain}" + type = "A" + + alias { + name = aws_cloudfront_distribution.waf.domain_name + zone_id = aws_cloudfront_distribution.waf.hosted_zone_id + evaluate_target_health = false + } +} + +resource "aws_acm_certificate" "subdomain" { + # Specify the name rather than referencing the resource directly. This allows + # us to create the certificate before the DNS record exists. + domain_name = "${local.subdomain}.${var.domain}" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "validation" { + for_each = { + for dvo in aws_acm_certificate.subdomain.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 300 + type = each.value.type + zone_id = data.aws_route53_zone.domain.zone_id +} + +resource "aws_acm_certificate_validation" "validation" { + certificate_arn = aws_acm_certificate.subdomain.arn + validation_record_fqdns = [ + for record in aws_route53_record.validation : record.fqdn + ] +} diff --git a/aws/cloudfront_waf/locals.tf b/aws/cloudfront_waf/locals.tf new file mode 100644 index 0000000..d71f618 --- /dev/null +++ b/aws/cloudfront_waf/locals.tf @@ -0,0 +1,5 @@ +locals { + subdomain = var.subdomain == "" ? var.environment : var.subdomain + origin_domain = var.origin_domain == "" ? "origin.${local.subdomain}.${var.domain}" : var.origin_domain + prefix = "${var.project}-${var.environment}" +} diff --git a/aws/cloudfront_waf/main.tf b/aws/cloudfront_waf/main.tf new file mode 100644 index 0000000..53fe4db --- /dev/null +++ b/aws/cloudfront_waf/main.tf @@ -0,0 +1,213 @@ +resource "aws_cloudfront_cache_policy" "waf_passthrough" { + name = "${var.project}-${var.environment}-waf-passthrough" + comment = "We don't really care about caching, we just want to pass traffic to the WAF." + default_ttl = 1 + max_ttl = 1 + min_ttl = 1 + + parameters_in_cache_key_and_forwarded_to_origin { + enable_accept_encoding_brotli = true + enable_accept_encoding_gzip = true + + cookies_config { + cookie_behavior = "all" + } + headers_config { + header_behavior = "whitelist" + headers { + items = ["Host"] + } + } + query_strings_config { + query_string_behavior = "all" + } + } +} + +resource "aws_cloudfront_distribution" "waf" { + enabled = true + comment = "Pass traffic through WAF before sending to the origin." + is_ipv6_enabled = true + aliases = ["${local.subdomain}.${var.domain}"] + price_class = "PriceClass_100" + web_acl_id = aws_wafv2_web_acl.waf.arn + + origin { + domain_name = local.origin_domain + origin_id = local.origin_domain + connection_attempts = 3 + connection_timeout = 10 + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_keepalive_timeout = 5 + origin_protocol_policy = "https-only" + origin_read_timeout = 30 + origin_ssl_protocols = ["TLSv1.2"] + } + } + + logging_config { + include_cookies = false + bucket = var.log_bucket + prefix = "cloudfront/${local.subdomain}.${var.domain}" + } + + default_cache_behavior { + cache_policy_id = aws_cloudfront_cache_policy.waf_passthrough.id + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD"] + target_origin_id = local.origin_domain + compress = true + default_ttl = 0 + max_ttl = 0 + min_ttl = 0 + + origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed_cors.id + response_headers_policy_id = data.aws_cloudfront_response_headers_policy.managed_cors.id + + viewer_protocol_policy = "redirect-to-https" + } + + restrictions { + geo_restriction { + restriction_type = "whitelist" + locations = ["US", "CA", "GB", "DE"] + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.subdomain.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } +} + +resource "aws_wafv2_web_acl" "waf" { + name = local.prefix + create_before_destroy = true + description = "Web application firewall rules for ${var.project}." + scope = "CLOUDFRONT" + + default_action { + allow {} + } + + rule { + name = "AWS-RateBasedRule-IP-300" + priority = 0 + + action { + count {} + } + + statement { + rate_based_statement { + aggregate_key_type = "IP" + evaluation_window_sec = 300 + limit = 300 + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf-rate-limit" + sampled_requests_enabled = true + } + } + + rule { + name = "AWS-AWSManagedRulesAmazonIpReputationList" + priority = 1 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesAmazonIpReputationList" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf-ip-reputation" + sampled_requests_enabled = true + } + } + + rule { + name = "AWS-AWSManagedRulesCommonRuleSet" + priority = 2 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesCommonRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf-common-rules" + sampled_requests_enabled = true + } + } + + rule { + name = "AWS-AWSManagedRulesKnownBadInputsRuleSet" + priority = 3 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesKnownBadInputsRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf-known-bad-inputs" + sampled_requests_enabled = true + } + } + + rule { + name = "AWS-AWSManagedRulesSQLiRuleSet" + priority = 4 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesSQLiRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf-sqli" + sampled_requests_enabled = true + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.prefix}-waf" + sampled_requests_enabled = true + } +} diff --git a/aws/cloudfront_waf/variables.tf b/aws/cloudfront_waf/variables.tf new file mode 100644 index 0000000..0763b0c --- /dev/null +++ b/aws/cloudfront_waf/variables.tf @@ -0,0 +1,32 @@ +variable "domain" { + type = string + description = "Domain used for this deployment." +} + +variable "environment" { + type = string + description = "Environment for the deployment." + default = "dev" +} + +variable "log_bucket" { + type = string + description = "S3 Bucket to send logs to." +} + +variable "origin_domain" { + type = string + description = "Origin domain this deployment will point to. Defaults to origin.subdomain.domain." + default = "" +} + +variable "project" { + type = string + description = "Project that these resources are supporting." +} + +variable "subdomain" { + type = string + description = "Subdomain used for this deployment. Defaults to the environment." + default = "" +} diff --git a/aws/cloudfront_waf/versions.tf b/aws/cloudfront_waf/versions.tf new file mode 100644 index 0000000..ff09141 --- /dev/null +++ b/aws/cloudfront_waf/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6" + + required_providers { + aws = { + version = ">= 5.44" + source = "hashicorp/aws" + } + } +}