hashicorp / terraform-provider-aws

The AWS Provider enables Terraform to manage AWS resources.
https://registry.terraform.io/providers/hashicorp/aws
Mozilla Public License 2.0
9.87k stars 9.21k forks source link

[Bug]: Waf rule creation with multiple statements failing with error. #32808

Open apunati opened 1 year ago

apunati commented 1 year ago

Terraform Core Version

v1.3.3

AWS Provider Version

v5.10.0

Affected Resource(s)

I am trying to create a waf_acl along with rule group by adding multiple rules. when I added rule with 'ratebased' statement along with regular expression pattern it created properly ,but when I added new rule 'ratebased' with 'ipset' for same rule group. I am getting error.

PS: I am using Terragrunt.

This is combination of statements is working fine, when I tried to create it through AWS console (manually), unfortunately through code it is giving error.

Sample code which I am using:

dynamic "rule" {
    for_each = each.value.rules
    content {
      visibility_config {
        cloudwatch_metrics_enabled = rule.value.visibility_config.cloudwatch_metrics_enabled
        metric_name                = rule.value.visibility_config.metric_name
        sampled_requests_enabled   = rule.value.visibility_config.sampled_requests_enabled
      }
      action {
        block {}
      }
      name     = rule.key
      priority = rule.value.priority

      statement {
        dynamic "rate_based_statement" {
          for_each = toset(flatten([
            for stkey, stval in rule.value.statements :
            stval.type == "testing" ? [stval] : []
          ]))
          content {
            limit              = rate_based_statement.value.limit
            aggregate_key_type = rate_based_statement.value.aggregate_key_type

            scope_down_statement {
              not_statement {
                statement {
                 ip_set_reference_statement {
                    arn = aws_wafv2_ip_set.iaac_waf_ipset[rate_based_statement.value.ipset].arn
                  }

                }
              }
            }
          }
        }
      }
    }
   }    

Expected Behavior

It should create WAF rule with ratebased and IPset statement combination.

Actual Behavior

getting Error :

Error: updating WAFv2 RuleGroup (dcd9111c-c738-442d-80d4-63de5ef05a75): WAFInvalidParameterException: Error reason: EXACTLY_ONE_CONDITION_REQUIRED, field: STATEMENT, parameter: Statement
│ {
│   RespMetadata: {
│     StatusCode: 400,
│     RequestID: "f9f3b8c0-79df-46c5-b96f-60f2df6e58c7"
│   },
│   Field: "STATEMENT",
│   Message_: "Error reason: EXACTLY_ONE_CONDITION_REQUIRED, field: STATEMENT, parameter: Statement",
│   Parameter: "Statement",
│   Reason: "You have used none or multiple values for a field that requires exactly one value."
│ }

Relevant Error/Panic Output Snippet

No response

Terraform Configuration Files

################## creation Regex set #########################
###############################################################
resource "aws_wafv2_regex_pattern_set" "iaac_regex_pattern" {
  for_each = var.regex_pattern_sets
  name     = each.key
  scope    = each.value.scope

  regular_expression {
    regex_string = each.value.expression
  }
}

################# Creation of ip-set ##########################
###############################################################

resource "aws_wafv2_ip_set" "iaac_waf_ipset" {
  for_each = var.ip_sets
  name     = each.key
  scope    = each.value.scope

  ip_address_version = each.value.ip_address_version
  addresses          = each.value.addresses
}

######################## waf_acl creation ######################
################################################################
resource "aws_wafv2_web_acl" "waf_acl" {
  for_each = var.waf_acl
  name  = each.key
  scope = each.value.scope

  default_action {
    block {}
  }

  dynamic "rule" {
    for_each = each.value.rules
    content {

    name     = rule.key
    priority = rule.value.priority

    override_action {
      count {}
    }

    statement {
      dynamic "rule_group_reference_statement" {
        for_each = toset(flatten([
                for stkey, stval in rule.value.statements :
                stval.type == "rule_group" ? [stval] : []
              ]))
      content {
        arn = aws_wafv2_rule_group.custom_rule_group[rule_group_reference_statement.value.rule_group].arn
      }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled =  rule.value.visibility_config.cloudwatch_metrics_enabled
      metric_name                = rule.value.visibility_config.metric_name
      sampled_requests_enabled   = rule.value.visibility_config.sampled_requests_enabled
    }
  }
  }

  visibility_config {
    cloudwatch_metrics_enabled = each.value.visibility_config.cloudwatch_metrics_enabled
    metric_name                = each.value.visibility_config.metric_name
    sampled_requests_enabled   = each.value.visibility_config.cloudwatch_metrics_enabled
  }
}

################ Creation Of Rule group ########################
################################################################

resource "aws_wafv2_rule_group" "custom_rule_group" {
  for_each = var.rule_group_configs
  visibility_config {
    cloudwatch_metrics_enabled = each.value.visibility_config.cloudwatch_metrics_enabled
    metric_name                = each.value.visibility_config.metric_name
    sampled_requests_enabled   = each.value.visibility_config.sampled_requests_enabled
  }
  capacity = each.value.capacity
  name     = each.key
  scope    = each.value.scope

 ################ ratebased rule along with regular expression pattern #############
  dynamic "rule" {
    for_each = each.value.rules
    content {
      visibility_config {
        cloudwatch_metrics_enabled = rule.value.visibility_config.cloudwatch_metrics_enabled
        metric_name                = rule.value.visibility_config.metric_name
        sampled_requests_enabled   = rule.value.visibility_config.sampled_requests_enabled
      }
      action {
        block {}
      }
      name     = rule.key
      priority = rule.value.priority

      statement {
        dynamic "rate_based_statement" {
          for_each = toset(flatten([
            for stkey, stval in rule.value.statements :
            stval.type == "ratebased" ? [stval] : []
          ]))
          content {
            limit              = rate_based_statement.value.limit
            aggregate_key_type = rate_based_statement.value.aggregate_key_type

            scope_down_statement {
              not_statement {
                statement {

                  regex_pattern_set_reference_statement {
                    arn = aws_wafv2_regex_pattern_set.iaac_regex_pattern[rate_based_statement.value.pattern].arn

                    field_to_match {
                      single_header {
                        name = rate_based_statement.value.header
                      }
                    }
                    text_transformation {
                      priority = 2
                      type     = rate_based_statement.value.transform
                    }
                  }
                }
              }
            }
          }
        }

        # not_statement {
        #   statement {

            dynamic "ip_set_reference_statement" {

              for_each = toset(flatten([
                for stkey, stval in rule.value.statements :
                stval.type == "ipset" ? [stval] : []
              ]))
              content {
                arn = aws_wafv2_ip_set.iaac_waf_ipset[ip_set_reference_statement.value.ipset].arn
              }
            }
        #   }
        # }
      }
    }
   }

   ######################### ratebased rule along with 
    dynamic "rule" {
    for_each = each.value.rules
    content {
      visibility_config {
        cloudwatch_metrics_enabled = rule.value.visibility_config.cloudwatch_metrics_enabled
        metric_name                = rule.value.visibility_config.metric_name
        sampled_requests_enabled   = rule.value.visibility_config.sampled_requests_enabled
      }
      action {
        block {}
      }
      name     = rule.key
      priority = rule.value.priority

      statement {
        dynamic "rate_based_statement" {
          for_each = toset(flatten([
            for stkey, stval in rule.value.statements :
            stval.type == "testing" ? [stval] : []
          ]))
          content {
            limit              = rate_based_statement.value.limit
            aggregate_key_type = rate_based_statement.value.aggregate_key_type

            scope_down_statement {
              not_statement {
                statement {
                 ip_set_reference_statement {
                    arn = aws_wafv2_ip_set.iaac_waf_ipset[rate_based_statement.value.ipset].arn
                  }

                }
              }
            }
          }
        }
      }
    }
   }   

---------------------------------------------------------------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------------------------------------
# TERRAGRUNT CONFIGURATION
# This is the configuration for Terragrunt, a thin wrapper for Terraform that helps keep your code DRY and
# maintainable: https://github.com/gruntwork-io/terragrunt
# ---------------------------------------------------------------------------------------------------------------------

# We override the terraform block source attribute here just for the dev environment to show how you would deploy a
# different version of the module in a specific environment.
terraform {
  source = "../../modules//waf/terragrunt_wrapper"
}

# ---------------------------------------------------------------------------------------------------------------------
# Include configurations that are common used across multiple environments.
# ---------------------------------------------------------------------------------------------------------------------

# Include the root `terragrunt.hcl` configuration. The root configuration contains settings that are common across all
# components and environments, such as how to configure remote state.
include "root" {
  path = find_in_parent_folders()
}

locals {
  # Automatically load environment-level variables
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))

  # Extract out common variables for reuse
  env              = local.environment_vars.locals.name_prefix
  region           = local.environment_vars.locals.aws_region
  region_secondary = local.environment_vars.locals.aws_region_secondary
  region_tertiary  = local.environment_vars.locals.aws_region_tertiary
  project          = local.environment_vars.locals.project
}

inputs = {
default_inputs = {}
  required_inputs = {
  aws_waf = {
  regex_pattern_sets = {
  testregex = {
    scope      = "CLOUDFRONT"
    expression = "/*/api/health"
  }
}
ip_sets = {
    testipset = {
        scope = "CLOUDFRONT"
        ip_address_version = "IPV4"
        addresses = [ "192.162.1.1/32", "192.162.2.1/32" ]
    }
}

waf_acl = {
  iaac_wafacl = {
     scope = "CLOUDFRONT"
     visibility_config = {
      cloudwatch_metrics_enabled = false
      metric_name                = "test"
      sampled_requests_enabled   = true
    }

    rules = {
      rule3 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 0
        statements = {
            statementC = {
                type = "rule_group"
                rule_group = "test"
            }
        }
      }
    }
  }
}

rule_group_configs = {
  test = {
    scope    = "CLOUDFRONT"
    capacity = 500
    visibility_config = {
      cloudwatch_metrics_enabled = false
      metric_name                = "test"
      sampled_requests_enabled   = true
    }

    rules = {

      rule1 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 1
        statements = {
            statementA = {
              type      = "ratebased"
              header    = "referer"
              transform = "NONE"
              pattern   = "testregex"
              aggregate_key_type = "IP"
              limit = 1000
            }
        }
      }

      rule2 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 2
        statements = {
            statementB = {
                type = "ipset"
                ipset = "testipset"
            }

        }
      }

      rule4 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 4
        statements = {
            statementD = {
              type      = "testing"
              ipset     = "testipset"
              aggregate_key_type = "IP"
              limit = 1000
            }
        }
      }

    }
  }
 }
}

  ######## waf_acl2 #################
  aws_waf2 = {
  regex_pattern_sets = {
  testregex1 = {
    scope      = "CLOUDFRONT"
    expression = "/*/api/health"
  }
}
ip_sets = {
    testipset1 = {
        scope = "CLOUDFRONT"
        ip_address_version = "IPV4"
        addresses = [ "192.162.1.1/32", "192.162.2.1/32" ]
    }
}

waf_acl = {
  iaac_wafacl2 = {
     scope = "CLOUDFRONT"
     visibility_config = {
      cloudwatch_metrics_enabled = false
      metric_name                = "test"
      sampled_requests_enabled   = true
    }

    rules = {
      rule3 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 0
        statements = {
            statementC = {
                type = "rule_group"
                rule_group = "test1"
            }
        }
      }
    }
  }
}

rule_group_configs = {
  test1 = {
    scope    = "CLOUDFRONT"
    capacity = 500
    visibility_config = {
      cloudwatch_metrics_enabled = false
      metric_name                = "test"
      sampled_requests_enabled   = true
    }

    rules = {

      rule1 = {
        visibility_config = {
          cloudwatch_metrics_enabled = false
          metric_name                = "test"
          sampled_requests_enabled   = true
        }
        priority = 1
        statements = {
            statementA = {
              type      = "ratebased"
              header    = "referer"
              transform = "NONE"
              pattern   = "testregex1"
              aggregate_key_type = "IP"
              limit = 1000
            }
        }
      }

Steps to Reproduce

Execute the below commands: terragrunt init terragrunt plan terragrunt apply

If you are using windows Env. Please use below command before init export TERRAGRUNT_DOWNLOAD=/c/tmp

Debug Output

No response

Panic Output

No response

Important Factoids

No response

References

No response

Would you like to implement a fix?

None

github-actions[bot] commented 1 year ago

Community Note

Voting for Prioritization

Volunteering to Work on This Issue

justinretzolk commented 1 year ago

Hey @apunati 👋 Thank you for taking the time to raise this! So that we have the necessary information in order to look into this, can you supply debug logging (redacted as needed) as well?

dragosrosculete commented 7 months ago

Similar issue experienced here. I am able to add the rule in WAF using AWS CLI . I am only managing the rules using aws cli .

I am creating the WAF using terraform but not the rule. Even so, terraform aws provider is trying to read the rules and it fails. Waiting for a fix for this. A solution ( only if not wanting to manage the rules) is to add an option to ignore reading the rules. I am only managing the WAF part in terraform , not the rules part.