From 8dae6145958b07a45ebed7997b75cf205d697316 Mon Sep 17 00:00:00 2001 From: olamide Date: Thu, 21 Mar 2024 15:59:00 +0100 Subject: [PATCH 01/20] Push WAF block logs to SNS --- .../lambda-script/lambda_function.py | 8 ++++++-- aws/waf/main.tf | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/aws/cloudwatch-log-extract/lambda-script/lambda_function.py b/aws/cloudwatch-log-extract/lambda-script/lambda_function.py index f5a50f0b..5224540f 100644 --- a/aws/cloudwatch-log-extract/lambda-script/lambda_function.py +++ b/aws/cloudwatch-log-extract/lambda-script/lambda_function.py @@ -40,8 +40,12 @@ def lambda_handler(event, context): if regex_pattern: matchObject = re.match(regex_pattern, log_message) if matchObject: - for key,value in capture_group.items(): - publish_dict[value] = matchObject.group(int(key)) + if capture_group: + for key,value in capture_group.items(): + publish_dict[value] = matchObject.group(int(key)) + else: + publish_dict["message"] = log_message + else: publish_dict["message"] = log_message diff --git a/aws/waf/main.tf b/aws/waf/main.tf index 2ac955c6..91d739ab 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -201,4 +201,19 @@ resource "aws_wafv2_ip_set" "block_ip_list" { scope = var.waf_scope ip_address_version = "IPV4" addresses = var.block_ip_list -} \ No newline at end of file +} + +module "cloudwatch_log_extract" { + source = "../cloudwatch-log-extract" + + source_cloudwatch_log_group = aws_cloudwatch_log_group.aws_waf_log_group.name + log_group_filter_pattern = "{ $.action = \"BLOCK\" }" + message_attributes = { + waf = aws_wafv2_web_acl.main.id + } + destination_sns_topic_arn = aws_sns_topic.waf_logs_sns_subscription.arn +} + +resource "aws_sns_topic" "waf_logs_sns_subscription" { + name = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" +} From 004b9e9def0394ed46ac9e10947870276e65d629 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Mar 2024 14:59:31 +0000 Subject: [PATCH 02/20] terraform-docs: automated action --- aws/waf/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aws/waf/README.md b/aws/waf/README.md index b57d386b..087557d8 100644 --- a/aws/waf/README.md +++ b/aws/waf/README.md @@ -63,11 +63,18 @@ Note: For each rule, if you are providing a country list, you can only specify e |------|---------| | [aws](#provider\_aws) | ~> 4.0 | +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [cloudwatch\_log\_extract](#module\_cloudwatch\_log\_extract) | ../cloudwatch-log-extract | n/a | + ## Resources | Name | Type | |------|------| | [aws_cloudwatch_log_group.aws_waf_log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_sns_topic.waf_logs_sns_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_ssm_parameter.aws_waf_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_wafv2_ip_set.allowed_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource | | [aws_wafv2_ip_set.block_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource | From b2234a09ab2f3644b4b0b38e8ee1f631efe6edd6 Mon Sep 17 00:00:00 2001 From: olamide Date: Fri, 22 Mar 2024 12:32:38 +0100 Subject: [PATCH 03/20] Update cloudwatch dependency to create permission for lambda to execute cloudwatch --- aws/cloudwatch-log-extract/main.tf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aws/cloudwatch-log-extract/main.tf b/aws/cloudwatch-log-extract/main.tf index 31c068e9..b06951cf 100644 --- a/aws/cloudwatch-log-extract/main.tf +++ b/aws/cloudwatch-log-extract/main.tf @@ -5,9 +5,11 @@ locals { resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_log_filter" { name = "cloudwatch_log_filter-${random_id.unique_id.dec}" - log_group_name = var.source_cloudwatch_log_group + log_group_name = data.aws_cloudwatch_log_group.log_group.name filter_pattern = var.log_group_filter_pattern destination_arn = aws_lambda_function.sql_query_update.arn + + depends_on = [ aws_lambda_permission.allow_cloudwatch_logs ] } resource "aws_lambda_function" "sql_query_update" { From 5f38066cce561e8bd8a82d8575819772093ffd6d Mon Sep 17 00:00:00 2001 From: olamide Date: Fri, 22 Mar 2024 13:21:02 +0100 Subject: [PATCH 04/20] Update source arn format for lambda permission statement --- aws/cloudwatch-log-extract/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/cloudwatch-log-extract/main.tf b/aws/cloudwatch-log-extract/main.tf index b06951cf..7ca05779 100644 --- a/aws/cloudwatch-log-extract/main.tf +++ b/aws/cloudwatch-log-extract/main.tf @@ -106,7 +106,7 @@ resource "aws_lambda_permission" "allow_cloudwatch_logs" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.sql_query_update.function_name principal = "logs.${data.aws_region.current.name}.amazonaws.com" - source_arn = data.aws_cloudwatch_log_group.log_group.arn + source_arn = "${data.aws_cloudwatch_log_group.log_group.arn}:*" } data "aws_region" "current" {} From c142db8a2d2ef0b306d1a36e20ec9bb9861526ab Mon Sep 17 00:00:00 2001 From: olamide Date: Fri, 22 Mar 2024 13:24:22 +0100 Subject: [PATCH 05/20] Update terraform fmt --- aws/cloudwatch-log-extract/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/cloudwatch-log-extract/main.tf b/aws/cloudwatch-log-extract/main.tf index 7ca05779..f9d4ecd1 100644 --- a/aws/cloudwatch-log-extract/main.tf +++ b/aws/cloudwatch-log-extract/main.tf @@ -9,7 +9,7 @@ resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_log_filter" { filter_pattern = var.log_group_filter_pattern destination_arn = aws_lambda_function.sql_query_update.arn - depends_on = [ aws_lambda_permission.allow_cloudwatch_logs ] + depends_on = [aws_lambda_permission.allow_cloudwatch_logs] } resource "aws_lambda_function" "sql_query_update" { From 99d23c9c2b1ccff679c88c7857e0d2b8fecc67f4 Mon Sep 17 00:00:00 2001 From: olamide Date: Fri, 22 Mar 2024 13:25:26 +0100 Subject: [PATCH 06/20] Update lambda permission statement --- aws/cloudwatch-log-extract/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/cloudwatch-log-extract/main.tf b/aws/cloudwatch-log-extract/main.tf index f9d4ecd1..ac3ba952 100644 --- a/aws/cloudwatch-log-extract/main.tf +++ b/aws/cloudwatch-log-extract/main.tf @@ -106,7 +106,7 @@ resource "aws_lambda_permission" "allow_cloudwatch_logs" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.sql_query_update.function_name principal = "logs.${data.aws_region.current.name}.amazonaws.com" - source_arn = "${data.aws_cloudwatch_log_group.log_group.arn}:*" + source_arn = "${data.aws_cloudwatch_log_group.log_group.arn}*" } data "aws_region" "current" {} From ccfdb9c1166abf190b8e7ca86f5033bb8bc62a69 Mon Sep 17 00:00:00 2001 From: olamide Date: Thu, 28 Mar 2024 20:06:01 +0100 Subject: [PATCH 07/20] Create SSM parameter for the SNS receiving WAF logs --- aws/waf/main.tf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index 91d739ab..a1bcac8b 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -217,3 +217,10 @@ module "cloudwatch_log_extract" { resource "aws_sns_topic" "waf_logs_sns_subscription" { name = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" } + +resource "aws_ssm_parameter" "aws_waf_acl" { + name = "/aws-waf/sns/${var.name}" + description = "Name of the SNS for the AWS WAF logs - ${var.name}" + type = "SecureString" + value = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" +} From a37faaa51ddb058170503da0494c8546d279395b Mon Sep 17 00:00:00 2001 From: olamide Date: Thu, 28 Mar 2024 20:10:42 +0100 Subject: [PATCH 08/20] Update ssm parameter resource name --- aws/waf/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index a1bcac8b..9f995887 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -218,7 +218,7 @@ resource "aws_sns_topic" "waf_logs_sns_subscription" { name = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" } -resource "aws_ssm_parameter" "aws_waf_acl" { +resource "aws_ssm_parameter" "aws_waf_sns_log" { name = "/aws-waf/sns/${var.name}" description = "Name of the SNS for the AWS WAF logs - ${var.name}" type = "SecureString" From 2681484807616b9b0ee904015b2b4d08122f32ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Mar 2024 19:11:34 +0000 Subject: [PATCH 09/20] terraform-docs: automated action --- aws/waf/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aws/waf/README.md b/aws/waf/README.md index 087557d8..a09f57c6 100644 --- a/aws/waf/README.md +++ b/aws/waf/README.md @@ -76,6 +76,7 @@ Note: For each rule, if you are providing a country list, you can only specify e | [aws_cloudwatch_log_group.aws_waf_log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_sns_topic.waf_logs_sns_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_ssm_parameter.aws_waf_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.aws_waf_sns_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_wafv2_ip_set.allowed_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource | | [aws_wafv2_ip_set.block_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource | | [aws_wafv2_web_acl.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl) | resource | From bd3b520a0ff279cde3a4bce398647580c7d1a545 Mon Sep 17 00:00:00 2001 From: olamide Date: Thu, 28 Mar 2024 20:14:15 +0100 Subject: [PATCH 10/20] Change ssm parameter name, aws-waf is a reserved parameter name --- aws/waf/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index 9f995887..f380e2ce 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -219,7 +219,7 @@ resource "aws_sns_topic" "waf_logs_sns_subscription" { } resource "aws_ssm_parameter" "aws_waf_sns_log" { - name = "/aws-waf/sns/${var.name}" + name = "/waflogs/sns/${var.name}" description = "Name of the SNS for the AWS WAF logs - ${var.name}" type = "SecureString" value = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" From aa22804a980dd1658b073547418c829079677fd5 Mon Sep 17 00:00:00 2001 From: olamide Date: Thu, 28 Mar 2024 20:25:08 +0100 Subject: [PATCH 11/20] Store SNS topic arn in SSM --- aws/waf/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index f380e2ce..f3760eb2 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -222,5 +222,5 @@ resource "aws_ssm_parameter" "aws_waf_sns_log" { name = "/waflogs/sns/${var.name}" description = "Name of the SNS for the AWS WAF logs - ${var.name}" type = "SecureString" - value = "${aws_wafv2_web_acl.main.id}-waf-logs-topic" + value = aws_sns_topic.waf_logs_sns_subscription.arn } From 28d12eb969f0524ed1cfb17fafed80c5a6e8703d Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 12:09:44 +0100 Subject: [PATCH 12/20] Add waf rule to inspect for header values --- aws/waf/main.tf | 64 ++++++++++++++++++++++++++++++++++++++++++++ aws/waf/variables.tf | 15 +++++++++++ 2 files changed, 79 insertions(+) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index f3760eb2..f632cab8 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -13,6 +13,70 @@ resource "aws_wafv2_web_acl" "main" { metric_name = "${var.name}-cloudfront-web-acl" } + dynamic "header_rule" { + for_each = var.header_match_rules + content { + name = "${header_rule.value["name"]}-header-match-rule" + priority = header_rule.value["priority"] + + dynamic "action" { + for_each = header_rule.value["count_override"] == true ? [1] : [] + content { + count {} + } + } + dynamic "action" { + for_each = header_rule.value["count_override"] == false ? [1] : [] + content { + block {} + } + } + statement { + byte_match_statement { + field_to_match { + single_header = lower(header_rule.value["header_name"]) + } + + positional_constraint = "CONTAINS" + + search_string = header_rule.value["header_value"] + + text_transformation { + priority = 1 + type = "LOWERCASE" + } + + dynamic "scope_down_statement" { + for_each = length(concat(rule.value["country_list"], rule.value["exempt_country_list"])) > 0 ? [1] : [] + content { + dynamic "geo_match_statement" { + for_each = length(rule.value["country_list"]) > 0 ? [1] : [] + content { + country_codes = rule.value["country_list"] + } + } + dynamic "not_statement" { + for_each = length(rule.value["exempt_country_list"]) > 0 ? [1] : [] + content { + statement { + geo_match_statement { + country_codes = rule.value["exempt_country_list"] + } + } + } + } + } + } + } + } + visibility_config { + cloudwatch_metrics_enabled = true + sampled_requests_enabled = true + metric_name = "${rule.value["name"]}-header-match-rule" + } + } + } + dynamic "rule" { for_each = var.rate_limit_rules content { diff --git a/aws/waf/variables.tf b/aws/waf/variables.tf index aafc5924..18bb1efe 100644 --- a/aws/waf/variables.tf +++ b/aws/waf/variables.tf @@ -43,6 +43,21 @@ variable "rate_limit_rules" { })) } +variable "header_match_rules" { + description = "Rule statement to inspect and match the header for an incoming request." + type = map(object({ + name = string # Name of the header match rule group + priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF. + header_name = string # This is the name of the header to inspect for all incoming requests. + header_value = string # This is the value to look out for a matching header name for all incoming requests + count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false. + country_list = optional(list(string), []) # List of countries to apply the header match to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both. + exempt_country_list = optional(list(string), []) # List of countries to exempt from the header match rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both. + })) + + default = null +} + variable "allowed_ip_list" { description = "List of allowed IP addresses, these IP addresses will be exempted from any configured rules" type = list(string) From 635befc960527073fb0b14fee098fa62dfce1ad3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Apr 2024 11:10:12 +0000 Subject: [PATCH 13/20] terraform-docs: automated action --- aws/waf/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aws/waf/README.md b/aws/waf/README.md index a09f57c6..64eb83ff 100644 --- a/aws/waf/README.md +++ b/aws/waf/README.md @@ -90,6 +90,7 @@ Note: For each rule, if you are providing a country list, you can only specify e | [allowed\_ip\_list](#input\_allowed\_ip\_list) | List of allowed IP addresses, these IP addresses will be exempted from any configured rules | `list(string)` | `[]` | no | | [aws\_managed\_rule\_groups](#input\_aws\_managed\_rule\_groups) | Rule statement values used to run the rules that are defined in a managed rule group. You may review this list for the available AWS managed rule groups - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Managed rule group
priority = number # Relative processing order for rules processed by AWS WAF. All rules are processed from lowest priority to the highest.
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`.
country_list = optional(list(string), []) # List of countries to apply the managed rule to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the managed rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [block\_ip\_list](#input\_block\_ip\_list) | List of IP addresses to be blocked and denied access to the ingress / cloudfront. | `list(string)` | `[]` | no | +| [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. |
map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
country_list = optional(list(string), []) # List of countries to apply the header match to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the header match rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| `null` | no | | [name](#input\_name) | Friendly name of the WebACL. | `string` | n/a | yes | | [rate\_limit\_rules](#input\_rate\_limit\_rules) | Rule statement to track and rate limits requests when they are coming at too fast a rate.. For more details, visit - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Rate limit rule group
priority = number # Relative processing order for rate limit rule relative to other rules processed by AWS WAF.
limit = optional(number, 2000) # This is the limit on requests from any single IP address within a 5 minute period
count_override = optional(bool, false) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
country_list = optional(list(string), []) # List of countries to apply the rate limit to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the rate limit. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [resource\_arn](#input\_resource\_arn) | The Amazon Resource Name (ARN) of the resource to associate with the web ACL. This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. Value is required if scope is REGIONAL | `string` | `null` | no | From 4641aeced1d8ff43e3eae6bdacacc43f605e7458 Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 12:33:03 +0100 Subject: [PATCH 14/20] Mark the header variable input as sensitive --- aws/waf/main.tf | 6 ++++-- aws/waf/variables.tf | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index f632cab8..cc3b8de9 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -13,7 +13,7 @@ resource "aws_wafv2_web_acl" "main" { metric_name = "${var.name}-cloudfront-web-acl" } - dynamic "header_rule" { + dynamic "rule" { for_each = var.header_match_rules content { name = "${header_rule.value["name"]}-header-match-rule" @@ -34,7 +34,9 @@ resource "aws_wafv2_web_acl" "main" { statement { byte_match_statement { field_to_match { - single_header = lower(header_rule.value["header_name"]) + single_header { + name = lower(header_rule.value["header_name"]) + } } positional_constraint = "CONTAINS" diff --git a/aws/waf/variables.tf b/aws/waf/variables.tf index 18bb1efe..ca8a825d 100644 --- a/aws/waf/variables.tf +++ b/aws/waf/variables.tf @@ -56,6 +56,8 @@ variable "header_match_rules" { })) default = null + + sensitive = true } variable "allowed_ip_list" { From 41bea1d8e5506eda5bc1885dcc77ac5e99c60f6c Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 13:00:59 +0100 Subject: [PATCH 15/20] Remove geo location scope down statement for header match block since it's not supported --- aws/waf/main.tf | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index cc3b8de9..a946cdd6 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -14,7 +14,8 @@ resource "aws_wafv2_web_acl" "main" { } dynamic "rule" { - for_each = var.header_match_rules + # for_each = var.header_match_rules == null ? {} : var.header_match_rules + for_each = var.header_match_rules == null ? {} : var.header_match_rules content { name = "${header_rule.value["name"]}-header-match-rule" priority = header_rule.value["priority"] @@ -47,28 +48,6 @@ resource "aws_wafv2_web_acl" "main" { priority = 1 type = "LOWERCASE" } - - dynamic "scope_down_statement" { - for_each = length(concat(rule.value["country_list"], rule.value["exempt_country_list"])) > 0 ? [1] : [] - content { - dynamic "geo_match_statement" { - for_each = length(rule.value["country_list"]) > 0 ? [1] : [] - content { - country_codes = rule.value["country_list"] - } - } - dynamic "not_statement" { - for_each = length(rule.value["exempt_country_list"]) > 0 ? [1] : [] - content { - statement { - geo_match_statement { - country_codes = rule.value["exempt_country_list"] - } - } - } - } - } - } } } visibility_config { From b36a6e511ca8eb345c9f1a38a185c9ebfa607d5a Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 13:08:50 +0100 Subject: [PATCH 16/20] Mark header_match_rules as non-sensitive so it can be used in dynamic loop --- aws/waf/variables.tf | 2 -- 1 file changed, 2 deletions(-) diff --git a/aws/waf/variables.tf b/aws/waf/variables.tf index ca8a825d..18bb1efe 100644 --- a/aws/waf/variables.tf +++ b/aws/waf/variables.tf @@ -56,8 +56,6 @@ variable "header_match_rules" { })) default = null - - sensitive = true } variable "allowed_ip_list" { From c84b89d4ef511d6e377354dae2e3c8b43e8837cb Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 15:03:20 +0100 Subject: [PATCH 17/20] Remove unsed variable values --- aws/waf/variables.tf | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/aws/waf/variables.tf b/aws/waf/variables.tf index 18bb1efe..dee871fb 100644 --- a/aws/waf/variables.tf +++ b/aws/waf/variables.tf @@ -46,13 +46,11 @@ variable "rate_limit_rules" { variable "header_match_rules" { description = "Rule statement to inspect and match the header for an incoming request." type = map(object({ - name = string # Name of the header match rule group - priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF. - header_name = string # This is the name of the header to inspect for all incoming requests. - header_value = string # This is the value to look out for a matching header name for all incoming requests - count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false. - country_list = optional(list(string), []) # List of countries to apply the header match to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both. - exempt_country_list = optional(list(string), []) # List of countries to exempt from the header match rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both. + name = string # Name of the header match rule group + priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF. + header_name = string # This is the name of the header to inspect for all incoming requests. + header_value = string # This is the value to look out for a matching header name for all incoming requests + count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false. })) default = null From 9bc991e91619b3094b9f50d018f6df758eed426b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Apr 2024 14:04:19 +0000 Subject: [PATCH 18/20] terraform-docs: automated action --- aws/waf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/waf/README.md b/aws/waf/README.md index 64eb83ff..19bb21e4 100644 --- a/aws/waf/README.md +++ b/aws/waf/README.md @@ -90,7 +90,7 @@ Note: For each rule, if you are providing a country list, you can only specify e | [allowed\_ip\_list](#input\_allowed\_ip\_list) | List of allowed IP addresses, these IP addresses will be exempted from any configured rules | `list(string)` | `[]` | no | | [aws\_managed\_rule\_groups](#input\_aws\_managed\_rule\_groups) | Rule statement values used to run the rules that are defined in a managed rule group. You may review this list for the available AWS managed rule groups - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Managed rule group
priority = number # Relative processing order for rules processed by AWS WAF. All rules are processed from lowest priority to the highest.
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`.
country_list = optional(list(string), []) # List of countries to apply the managed rule to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the managed rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [block\_ip\_list](#input\_block\_ip\_list) | List of IP addresses to be blocked and denied access to the ingress / cloudfront. | `list(string)` | `[]` | no | -| [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. |
map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
country_list = optional(list(string), []) # List of countries to apply the header match to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the header match rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| `null` | no | +| [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. |
map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
}))
| `null` | no | | [name](#input\_name) | Friendly name of the WebACL. | `string` | n/a | yes | | [rate\_limit\_rules](#input\_rate\_limit\_rules) | Rule statement to track and rate limits requests when they are coming at too fast a rate.. For more details, visit - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Rate limit rule group
priority = number # Relative processing order for rate limit rule relative to other rules processed by AWS WAF.
limit = optional(number, 2000) # This is the limit on requests from any single IP address within a 5 minute period
count_override = optional(bool, false) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
country_list = optional(list(string), []) # List of countries to apply the rate limit to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the rate limit. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [resource\_arn](#input\_resource\_arn) | The Amazon Resource Name (ARN) of the resource to associate with the web ACL. This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. Value is required if scope is REGIONAL | `string` | `null` | no | From 970f97b341380bc2cda596e51072ad0785995e55 Mon Sep 17 00:00:00 2001 From: olamide Date: Wed, 3 Apr 2024 18:10:01 +0100 Subject: [PATCH 19/20] Fix variable reference --- aws/waf/main.tf | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/aws/waf/main.tf b/aws/waf/main.tf index a946cdd6..fcf54bcc 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -14,20 +14,19 @@ resource "aws_wafv2_web_acl" "main" { } dynamic "rule" { - # for_each = var.header_match_rules == null ? {} : var.header_match_rules for_each = var.header_match_rules == null ? {} : var.header_match_rules content { - name = "${header_rule.value["name"]}-header-match-rule" - priority = header_rule.value["priority"] + name = "${rule.value["name"]}-header-match-rule" + priority = rule.value["priority"] dynamic "action" { - for_each = header_rule.value["count_override"] == true ? [1] : [] + for_each = rule.value["count_override"] == true ? [1] : [] content { count {} } } dynamic "action" { - for_each = header_rule.value["count_override"] == false ? [1] : [] + for_each = rule.value["count_override"] == false ? [1] : [] content { block {} } @@ -36,13 +35,13 @@ resource "aws_wafv2_web_acl" "main" { byte_match_statement { field_to_match { single_header { - name = lower(header_rule.value["header_name"]) + name = lower(rule.value["header_name"]) } } positional_constraint = "CONTAINS" - search_string = header_rule.value["header_value"] + search_string = rule.value["header_value"] text_transformation { priority = 1 From 0308b84ee18d2f78bd2051139c091d4c9ab781d0 Mon Sep 17 00:00:00 2001 From: Olamide Date: Fri, 5 Apr 2024 12:49:49 +0100 Subject: [PATCH 20/20] Enable header check for multiple headers --- aws/waf/README.md | 3 +- aws/waf/main.tf | 103 ++++++++++++++++++++++++++++++++++++++----- aws/waf/outputs.tf | 5 +++ aws/waf/variables.tf | 11 +++-- 4 files changed, 107 insertions(+), 15 deletions(-) diff --git a/aws/waf/README.md b/aws/waf/README.md index 19bb21e4..b5cfc8ec 100644 --- a/aws/waf/README.md +++ b/aws/waf/README.md @@ -90,7 +90,7 @@ Note: For each rule, if you are providing a country list, you can only specify e | [allowed\_ip\_list](#input\_allowed\_ip\_list) | List of allowed IP addresses, these IP addresses will be exempted from any configured rules | `list(string)` | `[]` | no | | [aws\_managed\_rule\_groups](#input\_aws\_managed\_rule\_groups) | Rule statement values used to run the rules that are defined in a managed rule group. You may review this list for the available AWS managed rule groups - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Managed rule group
priority = number # Relative processing order for rules processed by AWS WAF. All rules are processed from lowest priority to the highest.
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`.
country_list = optional(list(string), []) # List of countries to apply the managed rule to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the managed rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [block\_ip\_list](#input\_block\_ip\_list) | List of IP addresses to be blocked and denied access to the ingress / cloudfront. | `list(string)` | `[]` | no | -| [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. |
map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
}))
| `null` | no | +| [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. |
map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_values = map(object({ # Header values contains a map of headers to inspect. You can provide multiple headers and values, all headers will be inspected together with `AND` logic.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
not_statement = optional(bool, false) # This indicates if the result this header match should be negated. The negated result will be joined with other header match results using `AND` logic if more than 1 header is provided.
}))
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
}))
| `null` | no | | [name](#input\_name) | Friendly name of the WebACL. | `string` | n/a | yes | | [rate\_limit\_rules](#input\_rate\_limit\_rules) | Rule statement to track and rate limits requests when they are coming at too fast a rate.. For more details, visit - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html |
map(object({
name = string # Name of the Rate limit rule group
priority = number # Relative processing order for rate limit rule relative to other rules processed by AWS WAF.
limit = optional(number, 2000) # This is the limit on requests from any single IP address within a 5 minute period
count_override = optional(bool, false) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
country_list = optional(list(string), []) # List of countries to apply the rate limit to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
exempt_country_list = optional(list(string), []) # List of countries to exempt from the rate limit. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.
}))
| n/a | yes | | [resource\_arn](#input\_resource\_arn) | The Amazon Resource Name (ARN) of the resource to associate with the web ACL. This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. Value is required if scope is REGIONAL | `string` | `null` | no | @@ -101,4 +101,5 @@ Note: For each rule, if you are providing a country list, you can only specify e | Name | Description | |------|-------------| | [aws\_waf\_arn](#output\_aws\_waf\_arn) | The arn for AWS WAF WebACL. | +| [waf\_logs\_sns\_topic\_arn](#output\_waf\_logs\_sns\_topic\_arn) | The arn for the SNS topic to receive the AWS WAF logs | \ No newline at end of file diff --git a/aws/waf/main.tf b/aws/waf/main.tf index fcf54bcc..66ac941c 100644 --- a/aws/waf/main.tf +++ b/aws/waf/main.tf @@ -31,21 +31,104 @@ resource "aws_wafv2_web_acl" "main" { block {} } } - statement { - byte_match_statement { - field_to_match { - single_header { - name = lower(rule.value["header_name"]) + dynamic "statement" { + for_each = length(rule.value["header_values"]) == 1 ? rule.value["header_values"] : {} + content { + dynamic "byte_match_statement" { + for_each = statement.value["not_statement"] == false ? [1] : [] + content { + field_to_match { + single_header { + name = lower(statement.value["header_name"]) + } + } + + positional_constraint = "CONTAINS" + + search_string = statement.value["header_value"] + + text_transformation { + priority = 1 + type = "LOWERCASE" + } + } + } + dynamic "not_statement" { + for_each = statement.value["not_statement"] == true ? [1] : [] + content { + statement { + byte_match_statement { + field_to_match { + single_header { + name = lower(statement.value["header_name"]) + } + } + + positional_constraint = "CONTAINS" + + search_string = statement.value["header_value"] + + text_transformation { + priority = 1 + type = "LOWERCASE" + } + } + } } } + } + } + dynamic "statement" { + for_each = length(rule.value["header_values"]) > 1 ? [1] : [] + content { + and_statement { + dynamic "statement" { + for_each = rule.value["header_values"] + content { + dynamic "byte_match_statement" { + for_each = statement.value["not_statement"] == false ? [1] : [] + content { + field_to_match { + single_header { + name = lower(statement.value["header_name"]) + } + } - positional_constraint = "CONTAINS" + positional_constraint = "CONTAINS" - search_string = rule.value["header_value"] + search_string = statement.value["header_value"] - text_transformation { - priority = 1 - type = "LOWERCASE" + text_transformation { + priority = 1 + type = "LOWERCASE" + } + } + } + dynamic "not_statement" { + for_each = statement.value["not_statement"] == true ? [1] : [] + content { + statement { + byte_match_statement { + field_to_match { + single_header { + name = lower(statement.value["header_name"]) + } + } + + positional_constraint = "CONTAINS" + + search_string = statement.value["header_value"] + + text_transformation { + priority = 1 + type = "LOWERCASE" + } + } + } + } + } + } + } } } } diff --git a/aws/waf/outputs.tf b/aws/waf/outputs.tf index f0a1776f..dd7a7ccb 100644 --- a/aws/waf/outputs.tf +++ b/aws/waf/outputs.tf @@ -2,3 +2,8 @@ output "aws_waf_arn" { description = "The arn for AWS WAF WebACL." value = aws_wafv2_web_acl.main.arn } + +output "waf_logs_sns_topic_arn" { + description = "The arn for the SNS topic to receive the AWS WAF logs" + value = aws_sns_topic.waf_logs_sns_subscription.arn +} diff --git a/aws/waf/variables.tf b/aws/waf/variables.tf index dee871fb..69f11f71 100644 --- a/aws/waf/variables.tf +++ b/aws/waf/variables.tf @@ -46,10 +46,13 @@ variable "rate_limit_rules" { variable "header_match_rules" { description = "Rule statement to inspect and match the header for an incoming request." type = map(object({ - name = string # Name of the header match rule group - priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF. - header_name = string # This is the name of the header to inspect for all incoming requests. - header_value = string # This is the value to look out for a matching header name for all incoming requests + name = string # Name of the header match rule group + priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF. + header_values = map(object({ # Header values contains a map of headers to inspect. You can provide multiple headers and values, all headers will be inspected together with `AND` logic. + header_name = string # This is the name of the header to inspect for all incoming requests. + header_value = string # This is the value to look out for a matching header name for all incoming requests + not_statement = optional(bool, false) # This indicates if the result this header match should be negated. The negated result will be joined with other header match results using `AND` logic if more than 1 header is provided. + })) count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false. }))