Dynamically create multiple WAF rules with Terraform - amazon-web-services

I've got a piece of Terraform code that creates a Web ACL with a set of rules in AWS.
provider "aws" {
region = "eu-west-2"
}
resource "aws_wafv2_web_acl" "foo" {
name = "foo"
description = "foo"
scope = "REGIONAL"
default_action {
block {}
}
rule {
name = "AWS-AWSManagedRulesLinuxRuleSet"
priority = 0
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = "AWS-AWSManagedRulesLinuxRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "foo_name"
sampled_requests_enabled = false
}
}
rule {
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 1
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = "AWS-AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "foo_name"
sampled_requests_enabled = false
}
}
tags = {
Tag1 = "Value1"
}
visibility_config {
metric_name = "foo"
sampled_requests_enabled = false
cloudwatch_metrics_enabled = false
}
}
This works fine, but adding more rules means that my code starts to turn into somewhat of a monolith.
Is there a way to create multiple rules in Terraform using dynamic_blocks or for_each or something else, in a way that looks cleaner and dry?

You use dynamic in combination with for_each like this:
Define a variable:
variable "rules" {
type = list
default = [
{
name = "AWS-AWSManagedRulesLinuxRuleSet"
priority = 0
managed_rule_group_statement_name = "AWS-AWSManagedRulesLinuxRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 1
managed_rule_group_statement_name = "AWS-AWSManagedRulesSQLiRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
}
]
}
Then use it in the resource:
dynamic "rule" {
for_each = toset(var.rules)
content {
name = rule.value.name
priority = rule.value.priority
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = rule.value.managed_rule_group_statement_name
vendor_name = rule.value.managed_rule_group_statement_vendor_name
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = rule.value.metric_name
sampled_requests_enabled = false
}
}
}
(Note: Obviously this replaces your previous rule blocks. See also the documentation about Dynamic Blocks for more information.)

Related

How to create a WAF ACL with more than 1 managed statement

I'm trying to create a WAF ACL using two AWS Managed rules. These should be evaluated in natural order from priority 1 and then 2.
I've got:
resource "aws_wafv2_web_acl" "acl" {
name = "us-blog-production-waf-acl"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "managed-common-rules"
priority = 0
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "us-blog-production-managed-common-rules"
sampled_requests_enabled = true
}
}
rule {
name = "ip-reputation-rules"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesAmazonIpReputationList"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "us-blog-production-ip-reputation-rules"
sampled_requests_enabled = true
}
}
rule {
name = "acccount-takeover-rules"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesATPRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "us-blog-production-account-takeover-rules"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "us-blog-production-waf"
sampled_requests_enabled = false
}
}
I tried having two rule blocks inside the aws_wafv2_web_acl resource block but wouldn't work either.
The error I'm getting is:
│ {
│ RespMetadata: {
│ StatusCode: 400,
│ RequestID: "77a6b751-49b1-46d0-af20-39a25e578e79"
│ },
│ Field: "MANAGED_RULE_SET_STATEMENT",
│ Message_: "Error reason: A required field is missing from the parameter., field: MANAGED_RULE_SET_STATEMENT, parameter: ManagedRuleSetConfig",
│ Parameter: "ManagedRuleSetConfig",
│ Reason: "A required field is missing from the parameter."
│ }
How should I set it up?
As per my comment, the documentation says you can have multiple rules in the resource, but you have to have one of action or override_action [1]:
One of action or override_action is required when specifying a rule
This is what is missing in your code.
EDIT: The second issue that is happening is probably because there is additional pricing for the ATP managed rule set [2].
[1] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#rules
[2] https://aws.amazon.com/waf/pricing/#Intelligent_threat_mitigation_from_AWS_WAF

Error creating WAFv2 WebACL WAFInvalidParameterException

Error: Error creating WAFv2 WebACL: WAFInvalidParameterException: Error reason: You have used none or multiple values for a field that requires exactly one value., field: RULE_ACTION, parameter: RuleAction(block=null, allow=null, count=null)
{
RespMetadata: {
StatusCode: 400,
RequestID: "24106754-b0db-4497-8e19-e72f8908dc19"
},
Field: "RULE_ACTION",
Message_: "Error reason: You have used none or multiple values for a field that requires exactly one value., field: RULE_ACTION, parameter: RuleAction(block=null, allow=null, count=null)",
Parameter: "RuleAction(block=null, allow=null, count=null)",
Reason: "You have used none or multiple values for a field that requires exactly one value."
}
on .terraform/modules/wafv2/main.tf line 18, in resource "aws_wafv2_web_acl" "main":
18: resource "aws_wafv2_web_acl" "main" {
I am having this error while trying to deploy a WAFV2 with terraform any help is appreciated please.
Here is a little portion of the WAFv2 code:
resource "aws_wafv2_web_acl" "main" {
name = var.name
description = "WAFv2 ACL for ${var.name}"
scope = var.scope
default_action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
sampled_requests_enabled = true
metric_name = var.name
}
dynamic "rule" {
for_each = var.managed_rules
content {
name = rule.value.name
priority = rule.value.priority
override_action {
dynamic "none" {
for_each = rule.value.override_action == "none" ? [1] : []
content {}
}
I am trying to figure out why the error is still reflecting maybe a problem with my WAFV2?
There may be a number of reasons why this error happens, so without seeing the full Terraform it is a bit hard to tell what is going on.
I've seen this happen where my ACL contained two rules: a rule_group_reference_statement and a rate_based_statement.
My problem was the rule group reference needed an override_action:
override_action {
none {}
}
I didn't realize either that or an action was required, but I found out about that here: https://github.com/hashicorp/terraform-provider-aws/issues/14094#issuecomment-655625254
I was using the Java version of the CDK and the way to get the "none {}" override action is unclear from the documentation. But this works:
.overrideAction(CfnWebACL.OverrideActionProperty.builder()
.none(new HashMap<String, Object>())
.build())
resource "aws_wafv2_ip_set" "ip_whitelist" {
name = "ip-whitelist"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = ["100.12.10.20/32"] # your ip
}
resource "aws_wafv2_web_acl" "web_acl" {
name = "waf-rules"
description = "waf rules"
scope = "REGIONAL"
default_action {
allow {}
}
# ipsets
rule {
name = "ip-whitelist"
priority = 0
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.ip_whitelist.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "Whitelist-ip"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "white-list-ip"
sampled_requests_enabled = true
}
dynamic "rule" {
for_each = var.rules
content {
name = rule.value.name
priority = rule.value.priority
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = rule.value.managed_rule_group_statement_name
vendor_name = rule.value.managed_rule_group_statement_vendor_name
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = rule.value.metric_name
sampled_requests_enabled = true
}
}
}
}
# variables.tf
variable "rules" {
type = list(any)
default = [
{
name = "AWS-AWSManagedRulesAdminProtectionRuleSet"
priority = 1
managed_rule_group_statement_name = "AWSManagedRulesAdminProtectionRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesPHPRuleSet"
priority = 2
managed_rule_group_statement_name = "AWSManagedRulesPHPRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesLinuxRuleSet"
priority = 3
managed_rule_group_statement_name = "AWSManagedRulesLinuxRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesAmazonIpReputationList"
priority = 4
managed_rule_group_statement_name = "AWSManagedRulesAmazonIpReputationList"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 5
managed_rule_group_statement_name = "AWSManagedRulesSQLiRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesUnixRuleSet"
priority = 6
managed_rule_group_statement_name = "AWSManagedRulesUnixRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 7
managed_rule_group_statement_name = "AWSManagedRulesCommonRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
}
]
}

When using Terraform with AWS, how can I set a rate limit on a specific URI path (or regex of a URI path) on an ALB

I am trying to rate limit requests to the forgot password change URL using WAFv2 rules attached to an ALB on Cloudfront.
What I think I need to do is..
Create two resources aws_wafv2_web_acl.afv2_rate_limit and another called aws_wafv2_regex_pattern_set.wafv2_password_url
Example of rate: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl
Example of regex: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_regex_pattern_set
Combine these into a rule group, call it aws_wafv2_rule_group.wafv2_rule_group_pw_rate_group
Example of group: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_rule
I've created the rate limit and the regex, but I am failing to create the rule group. I put this rule in to refer to the rate limit
rule {
name = "rate_limit"
priority = 1
action {
block {}
}
statement {
and_statement {
statement {
rule_group_reference_statement { # !!FIXME!! doesn't work
arn = aws_wafv2_web_acl.wafv2_rate_limit.arn
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "password_url"
sampled_requests_enabled = false
}
}
I get the error on the rule_group_reference_statement line:
Blocks of type "rule_group_reference_statement" are not expected here.
I can attach the rule group to the ALB.
of course, the first question is whether this is even the right way to go about it?!
thanks for any thoughts.
working!
resource "aws_wafv2_web_acl" "wafv2_alb_pw5pm_acl" {
name = "wafv2_alb_pw5pm-acl"
description = "prevent brute forcing password setting or changing"
scope = "REGIONAL" # if using this, no need to set provider
default_action {
allow {} # pass traffic until the rules trigger a block
}
rule {
name = "rate_limit_pw5pm"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
#limit = 300 # 5 per sec = 300 per min
limit = 100 # smallest value for testing
aggregate_key_type = "IP"
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.wafv2_password_uri.arn
text_transformation {
priority = 1
type = "NONE"
}
field_to_match {
uri_path {}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_rule_vis"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_vis"
sampled_requests_enabled = false
}
tags = {
managedby = "terraform"
}
}
resource "aws_wafv2_web_acl_association" "web_acl_association_my_lb" {
resource_arn = aws_lb.xxxxxx.arn
web_acl_arn = aws_wafv2_web_acl.wafv2_alb_pw5pm_acl.arn
}
You can't nest a rule_group_reference_statement, for example for use inside a and_statement, not_statement or or_statement. It can only be referenced as a top-level statement within a rule.
yes you can. basically you need to declare an aws_wafv2_regex_pattern_set, in this example I use the URI "/api/*" but it can be a fixed one too.
resource "aws_wafv2_regex_pattern_set" "regex_pattern_api" {
name = "regex-path-api"
scope = "REGIONAL"
regular_expression {
regex_string = "/api/.+"
}
}
and the here is an example on how to use it on a waf declaration:
resource "aws_wafv2_web_acl" "waf" {
name = "waf"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "RateLimit"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
aggregate_key_type = "IP"
limit = 100
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.regex_pattern_api.arn
field_to_match {
uri_path {}
}
text_transformation {
priority = 1
type = "NONE"
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "waf"
sampled_requests_enabled = false
}
}
The cool part of this is that it is a rate limit that narrows the filter based on client IP using scope_down_statement

Error when using aws wafv2 create-web-acl

I am using version 2.0.50 of the AWS cli on Linux, and trying to create a v2 AWS WAF. Running the command aws wafv2 create-web-acl --cli-input-json file://waf.json results in the following response:
An error occurred (WAFInvalidParameterException) when calling the CreateWebACL operation: Error reason: Your statement has multiple values set for a field that requires exactly one value., field: RULE, parameter: Rule
Can somebody identify what is wrong with the following JSON, or confirm that they are seeing the same issue?
{
"DefaultAction": {
"Allow": {}
},
"Name": "test-web-acl",
"Rules": [
{
"Name": "rule-one",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"Name": "AWSManagedRulesUnixRuleSet",
"VendorName": "AWS"
}
},
"VisibilityConfig": {
"CloudWatchMetricsEnabled": false,
"MetricName": "rule-one-metric",
"SampledRequestsEnabled": false
}
}
],
"Scope": "REGIONAL",
"VisibilityConfig": {
"CloudWatchMetricsEnabled": false,
"MetricName": "test-web-acl-metric",
"SampledRequestsEnabled": false
}
}
I can't see what is incorrect about the JSON according to the syntax described here CreateWebACL
The answer is that the OverrideAction attribute is missing from the Rule object. When adding "OverrideAction":{"None":{}} to the Rule object, then the ACL was created. The error message is misleading.
# resources.tf
resource "aws_wafv2_ip_set" "ip_whitelist" {
name = var.waf_name
scope = var.waf_scope
ip_address_version = var.waf_ip_address_version
addresses = [var.waf_addresses]
}
resource "aws_wafv2_web_acl" "web_acl" {
name = var.waf_web_acl_name
description = var.waf_description
scope = var.waf_scope
default_action {
allow {}
}
# ipsets
rule {
name = var.waf_name
priority = 0
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.ip_whitelist.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = var.waf_name
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = var.waf_name
sampled_requests_enabled = true
}
dynamic "rule" {
for_each = var.rules
content {
name = rule.value.name
priority = rule.value.priority
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = rule.value.managed_rule_group_statement_name
vendor_name = rule.value.managed_rule_group_statement_vendor_name
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = rule.value.metric_name
sampled_requests_enabled = true
}
}
}
}
resource "aws_wafv2_web_acl_association" "waf_alb" {
resource_arn = aws_alb.alb.arn
web_acl_arn = aws_wafv2_web_acl.web_acl.arn
}
# variables.tf
# waf ipset name
variable "waf_name" {
type = string
default = "ip-whitelist"
}
variable "waf_scope" {
type = string
default = "REGIONAL"
}
variable "waf_ip_address_version" {
default = "IPV4"
type=string
}
variable "waf_addresses" {
default = "your-ip/32"
type = string
}
# waf details
variable "waf_web_acl_name" {
type=string
default = "waf-rules"
}
variable "waf_description" {
type=string
default = "waf rules"
}
# waf multiple rules
variable "rules" {
type = list(any)
default = [
{
name = "AWS-AWSManagedRulesAdminProtectionRuleSet"
priority = 1
managed_rule_group_statement_name = "AWSManagedRulesAdminProtectionRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesPHPRuleSet"
priority = 2
managed_rule_group_statement_name = "AWSManagedRulesPHPRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesLinuxRuleSet"
priority = 3
managed_rule_group_statement_name = "AWSManagedRulesLinuxRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesAmazonIpReputationList"
priority = 4
managed_rule_group_statement_name = "AWSManagedRulesAmazonIpReputationList"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 5
managed_rule_group_statement_name = "AWSManagedRulesSQLiRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesUnixRuleSet"
priority = 6
managed_rule_group_statement_name = "AWSManagedRulesUnixRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 7
managed_rule_group_statement_name = "AWSManagedRulesCommonRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
}
]
}

How can I iterate through a map variable in terraform

Im trying to iterate through a variable type map and i'm not sure how to
This is what i have so far
In my main.tf:
resource "aws_route_53_record" "proxy_dns" {
count = "${length(var.account_name)}"
zone_id = "${infrastructure.zone_id}"
name = "proxy-${element(split(",", var.account_name), count.index)}-dns
type = CNAME
ttl = 60
records = ["{records.dns_name}"]
}
And in my variables.tf
variable "account_name" {
type = "map"
default = {
"account1" = "accountA"
"account2" = "accountB"
}
}
I want to be able to create multiple resources with the different account names
If you are using Terraform 0.12.6 or later then you can use for_each instead of count to produce one instance for each element in your map:
resource "aws_route53_record" "proxy_dns" {
for_each = var.account_name
zone_id = infrastructure.zone_id
name = "proxy-${each.value}-dns"
# ... etc ...
}
The primary advantage of for_each over count is that Terraform will identify the instances by the key in the map, so you'll get instances like aws_route53_record.proxy_dns["account1"] instead of aws_route53_record.proxy_dns[0], and so you can add and remove elements from your map in future with Terraform knowing which specific instance belongs to each element.
each.key and each.value in the resource type arguments replace count.index when for_each is used. They evaluate to the key and value of the current map element, respectively.
You can use a combination of map, keys function,index function, and count. This terraform creates 3 acls with various rules.
The names of the acl's are determined by the keys.
The number of acl's is determined by the count of the keys.
The index of each rule (priority) is determined by the index function
The name of each rule is from the CONTAINS_WORD or CONTAINS property in the map
=>
variable "acls" {
type = map(any)
default = {
"acl1" = {
"CONTAINS_WORD" = ["api","aaa", "bbb", "ccc"]
"CONTAINS" = ["xxx","yyy"]
}
"acl2" = {
"CONTAINS_WORD" = [ "url1,"url2","url3"]
"CONTAINS" = ["url4"]
}
"acl3" = {
"CONTAINS_WORD" = ["xxx"]
"CONTAINS" = []
}
}
}
resource "aws_wafv2_web_acl" "acl" {
name = keys(var.acls)[count.index]
scope = "REGIONAL"
count = length(keys(var.acls))
default_action {
block {}
}
dynamic "rule" {
for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)
content {
name = rule.key
priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD, rule.key)
action {
allow {}
}
statement {
#https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
byte_match_statement {
positional_constraint = "CONTAINS_WORD"
search_string = lower(rule.key)
field_to_match {
uri_path {}
}
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}-${rule.key}"
sampled_requests_enabled = true
}
}
}
dynamic "rule" {
for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS)
content {
name = replace(rule.key, ".", "_")
priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS, rule.key) + length(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)
action {
allow {}
}
statement {
#https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
byte_match_statement {
positional_constraint = "CONTAINS"
search_string = lower(rule.key)
field_to_match {
uri_path {}
}
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}-${replace(rule.key, ".", "_")}"
sampled_requests_enabled = true
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}"
sampled_requests_enabled = true
}
}
Make the variable a list instead of a map. Maps are used to reference a name to a value. Lists are better for iterating over via a count method.
variable "account_name" {
type = "list"
default = {"accountA","accountB"}
}
resource "aws_route_53_record" "proxy_dns" {
count = "${length(var.account_name)}"
zone_id = "${infrastructure.zone_id}"
name = "proxy-${element(var.account_name, count.index)}-dns
type = CNAME
ttl = 60
records = ["{records.dns_name}"]
}