cloudposse / terraform-aws-waf

https://cloudposse.com/accelerate
Apache License 2.0
41 stars 57 forks source link

When `redacted_fields` variable property `single_header` list has more than one header item #84

Open kevinneufeld opened 5 months ago

kevinneufeld commented 5 months ago

Describe the Bug

When single_header property has more than one header in the list, Terraform PLAN fails with Error: Too many single_header blocks

 Error: Too many single_header blocks

   on ../../main.tf line 85, in resource "aws_wafv2_web_acl_logging_configuration" "default":
   85:         content {

No more than 1 "single_header" blocks are allowed

Expected Behavior

Since redacted_fields can only accept one block of each property type ( method, uri_path, query_string and single_header), the expectation is for multiple redacted_fields for each single_header

# When `redacted_fields` is:
redacted_fields = {
  default = {
     single_header = [
       "accept-encoding",
       "accept-language"
     ]
  }
}

# The plan should be something like this:
# module.waf.aws_wafv2_web_acl_logging_configuration.default[0] will be created
  + resource "aws_wafv2_web_acl_logging_configuration" "default" {
      + id                      = (known after apply)
      + log_destination_configs = (known after apply)
      + resource_arn            = (known after apply)

      + redacted_fields {
          + single_header {
              + name = "accept-encoding"
            }
        }
      + redacted_fields {
          + single_header {
              + name = "accept-language"
            }
        }
    }
  ...truncated output

Steps to Reproduce

# examples/complete/main.tf
provider "aws" {
  region = var.region
}

resource "aws_cloudwatch_log_group" "default" {
  # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration#log_destination_configs
  # data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`
  name = format("aws-waf-logs-%s", module.this.id)
  tags = module.this.tags
}

module "waf" {
  source = "../.."

  visibility_config = {
    cloudwatch_metrics_enabled = false
    metric_name                = "rules-example-metric"
    sampled_requests_enabled   = false
  }

  log_destination_configs = [aws_cloudwatch_log_group.default.arn]
  redacted_fields = {
    default = {
     single_header = [
       "accept-encoding",
       "accept-language"
     ]
    }
  }

  # https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html
  managed_rule_group_statement_rules = [
    {
      name     = "AWS-AWSManagedRulesAdminProtectionRuleSet"
      priority = 1

      statement = {
        name        = "AWSManagedRulesAdminProtectionRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "AWS-AWSManagedRulesAdminProtectionRuleSet"
      }
    },
    {
      name     = "AWS-AWSManagedRulesAmazonIpReputationList"
      priority = 2

      statement = {
        name        = "AWSManagedRulesAmazonIpReputationList"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "AWS-AWSManagedRulesAmazonIpReputationList"
      }
    },
    {
      name     = "AWS-AWSManagedRulesCommonRuleSet"
      priority = 3

      statement = {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "AWS-AWSManagedRulesCommonRuleSet"
      }
    },
    {
      name     = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
      priority = 4

      statement = {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
      }
    },
    # https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-bot.html
    {
      name     = "AWS-AWSManagedRulesBotControlRuleSet"
      priority = 5

      statement = {
        name        = "AWSManagedRulesBotControlRuleSet"
        vendor_name = "AWS"

        rule_action_override = {
          CategoryHttpLibrary = {
            action = "block"
            custom_response = {
              response_code = "404"
              response_header = {
                name  = "example-1"
                value = "example-1"
              }
            }
          }
          SignalNonBrowserUserAgent = {
            action = "count"
            custom_request_handling = {
              insert_header = {
                name  = "example-2"
                value = "example-2"
              }
            }
          }
        }

        managed_rule_group_configs = [
          {
            aws_managed_rules_bot_control_rule_set = {
              inspection_level = "COMMON"
            }
          }
        ]
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "AWS-AWSManagedRulesBotControlRuleSet"
      }
    }
  ]

  byte_match_statement_rules = []
  rate_based_statement_rules = []
  size_constraint_statement_rules = []
  xss_match_statement_rules = []
  sqli_match_statement_rules = []
  geo_match_statement_rules = []
  geo_allowlist_statement_rules = []
  regex_match_statement_rules = []
  ip_set_reference_statement_rules = []

  context = module.this.context
}

Screenshots

No response

Environment

No response

Additional Context

One possible (BREAKING) fix would be to change the redacted_fields from a map object to just an object and separate the dynamic blocks.

# variables Line#997: https://github.com/cloudposse/terraform-aws-waf/blob/main/variables.tf#L977
variable "redacted_fields" {
  type = object({
    method        = optional(bool, false)
    uri_path      = optional(bool, false)
    query_string  = optional(bool, false)
    single_header = optional(list(string), [])
  })

  default  = {}
  nullable = false
}

# main.tf Line#22: https://github.com/cloudposse/terraform-aws-waf/blob/main/main.tf#L22
resource "aws_wafv2_web_acl_logging_configuration" "default" {
  count = local.enabled && length(var.log_destination_configs) > 0 ? 1 : 0

  resource_arn            = one(aws_wafv2_web_acl.default[*].arn)
  log_destination_configs = var.log_destination_configs

  # Method
  dynamic "redacted_fields" {
    for_each = var.redacted_fields.method ? [true] : []
    content {
      method {}
    }
  }
  # Query String
  dynamic "redacted_fields" {
    for_each = var.redacted_fields.query_string ? [true] : []
    content {
      query_string {}
    }
  }
  # Uri Path
  dynamic "redacted_fields" {
    for_each = var.redacted_fields.uri_path ? [true] : []
    content {
      uri_path {}
    }
  }
  # Single Header
  dynamic "redacted_fields" {
    for_each = toset(var.redacted_fields.single_header)
    content {
      single_header {
        name = redacted_fields.value
      }
    }
  }
... truncated - the rest of the code is not included.
addepar-tg commented 2 months ago

We are also hitting this error with multiple values in the list. While we're able to filter out these headers during log ingestion, it would be great to be able to prevent these from logging to our S3 bucket as well. 👍