How to set up AWS WAF v2 with Terraform
Tags: aws, security, terraform, cloud
Published: 2022-07-15 03:33:00

## Introduction
I recently set up AWS WAF v2 and then found it to be a very useful service. I introduce it in this blog!

So far, I have been using professional security vendor-managed rules, but this time I deployed it using the rulesets provided by AWS(AWS Managed Rules), which I found easy to use and very convenient. In this article, I will guide you through the process of building a WAF with AWS Managed Rules via Terraform.

### Disclaimer
WAF is a significant service that governs security. What I will set up in this article is not always correct for every case and depends on the product you are setting up.

Setting up a WAF is your own responsibility, and if you are not confident in your ability to self-set up a WAF given the risks involved, I recommend an approach where you enlist the help of a vendor.

__Reference__
* [What is AWS WAF?](https://aws.amazon.com/waf/?nc1=h_ls)
* For those who don't be good at security knowledge, so you can use [security vendor consults](https://aws.amazon.com/waf/partners/?blog-posts-cards.sort-by=item.additionalFields.modifiedDate&blog-posts-cards.sort-order=desc&partner-solutions-cards.sort-by=item.additionalFields.partnerNameLower&partner-solutions-cards.sort-order=asc&awsf.partner-solutions-filter-partner-type-storage=*all&awsf.partner-solutions-filter-partner-categories=use-case%23waf-managed-rules&awsf.partner-solutions-filter-partner-location-storage=*all) who AWS WAF security partners are.



## How to Set up
1. [Create a Web ACL](#chap1)
1. [Create RuleSets](#chap2)
1. [Attach to CloudFront and ALB & Setting monitoring option](#chap3)

### Sample Codes
Here are [sample codes.](https://gist.github.com/masayoshi644/13e9d17efec808f05e98b057439edfc1) I also wrote an example in the blog, but it would be too long to write everything, so I have abbreviated some of the information. If you want to see the full version, please refer to this GitHub Gist.

### 1. Create a Web ACL <a name="chap1"></a>
Web ACL is a central resource. 
![WAF image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umjp9pbw70cmwrae6igo.png)

That provides the following features and so on.
* Dashboard
* Which rule sets to apply
* Which AWS resources to configure
* Log and Metrics settings

The setting is so simple, so you can use Management Console easily. As for me, I was set up by Terraform. Here is the document, [aws_wafv2_web_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl)

```json
resource "aws_wafv2_web_acl" "this" {
  name  = "${var.prefix}-${var.name}"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  tags = {
    Name = "${var.prefix}-${var.name}"
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = var.metric_name
    sampled_requests_enabled   = true
  }
}

```

The above sample is modularized and is used in the form of a call from the main file that serves as the entry point.

```json
module "waf_alb" {
  source        = "../../modules/waf_alb"
  prefix        = local.prefix
  name          = "waf-alb"
  metric_name   = "waf-alb"
  alb_ipsets_v4 = module.waf_ipsets.waf_ipsets_alb_ipv4_arn
  alb_ipsets_v6 = module.waf_ipsets.waf_ipsets_alb_ipv6_arn
  bucket = "aws-waf-logs-api-server-dev-blah-blah-blah
}

```


## 2. Create a Rulesets <a name="chap2"></a>
I use two types of Rulesets, "AWS Managed Rules" and "my own rules."
As the Disclaimer described, the rulesets are so IMPORTANT.
Above all, you have to set appropriate rules, but I hope my setting rules help you something.

### My own rules
I set three types of rules.

| name | rule |
|:-----|:-----|
|AWSRateBasedRuleDomesticDOS|prevent DOS from domestic|
|AWSRateBasedRuleGlobalDOS | prevent DOS from global|
|AWSIPBlackList|prevent Attack based on IP based| 

Here are Terraform codes; These attributes are also in [aws_wafv2_web_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl).

```json
  rule {
    name     = "AWSRateBasedRuleDomesticDOS"
    priority = 1

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"

        scope_down_statement {
          geo_match_statement {
            country_codes = ["JP"]
          }
        }
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSRateBasedRuleDomesticDOS"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "AWSRateBasedRuleGlobalDOS"
    priority = 2

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 500
        aggregate_key_type = "IP"

        scope_down_statement {
          not_statement {
            statement {
              geo_match_statement {
                country_codes = ["JP"]
              }
            }
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSRateBasedRuleGlobalDOS"
      sampled_requests_enabled   = true
    }
  }

```

These two rules can prevent DOS attacks based on `rate_based_statement.limit` and `country_codes`. The web service I have set up this time is basically intended only for use from my home country(Japan), so I have divided the range of the rate limit between Japan and overseas. 

However, the `country_codes` may be spoofed, and since the connection restriction by this rule is a time-limited block of 5 minutes, other countermeasures may be safer in case of targeted attacks.

As for the rate based-type detailed rule, please refer to the [document](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based.html).

That's why I set the third rule, `AWSIPBlaskList`.
In this case, the rule "IP set match" is used to create a list of IPs and prohibit the request from the IP sets.
Here is [document](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-ipset-match.html)

```json
rule {
  name     = "AWSIPBlackList"
  priority = 3

  action {
    block {}
  }

  statement {
    or_statement {
      statement {
        ip_set_reference_statement {
          arn = var.alb_ipsets_v4
        }
      }
      statement {
        ip_set_reference_statement {
          arn = var.alb_ipsets_v6
        }
      }
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "AWSIPBlackList"
    sampled_requests_enabled   = true
  }
}

```

Also, I prepare another module "waf_ipsets" so that I manage blacklist IP sets for utilizing the above rule.
```json
provider "aws" {
  alias  = "us-east-1"
  region = "us-east-1"
}

resource "aws_wafv2_ip_set" "blacklist_alb_ipv4" {
  name               = "${var.prefix}-alb-ipv4"
  scope              = "REGIONAL"
  ip_address_version = "IPV4"
  addresses          = ["127.0.0.1/32"]

  tags = {
    Name = "${var.prefix}-alb-ipv4"
  }
}

resource "aws_wafv2_ip_set" "blacklist_cf_ipv4" {
  name               = "${var.prefix}-cf-ipv4"
  scope              = "CLOUDFRONT"
  provider           = aws.us-east-1
  ip_address_version = "IPV4"
  addresses          = ["127.0.0.1/32"]

  tags = {
    Name = "${var.prefix}-cf-ipv4"
  }
}

resource "aws_wafv2_ip_set" "blacklist_alb_ipv6" {
  name               = "${var.prefix}-alb-ipv6"
  scope              = "REGIONAL"
  ip_address_version = "IPV6"
  addresses          = ["2001:0db8:0000:0000:0000:0000:0000:0001/128"]

  tags = {
    Name = "${var.prefix}-alb-ipv6"
  }
}

resource "aws_wafv2_ip_set" "blacklist_cf_ipv6" {
  name               = "${var.prefix}-cf-ipv6"
  scope              = "CLOUDFRONT"
  provider           = aws.us-east-1
  ip_address_version = "IPV6"
  addresses          = ["2001:0db8:0000:0000:0000:0000:0000:0001/128"]

  tags = {
    Name = "${var.prefix}-cf-ipv6"
  }
}

// ARN
output "waf_ipsets_alb_ipv4_arn" {
  value       = aws_wafv2_ip_set.blacklist_alb_ipv4.arn
  description = "IP sets arn"
}

output "waf_ipsets_cf_ipv4_arn" {
  value       = aws_wafv2_ip_set.blacklist_cf_ipv4.arn
  description = "IP sets arn"
}

output "waf_ipsets_alb_ipv6_arn" {
  value       = aws_wafv2_ip_set.blacklist_alb_ipv6.arn
  description = "IP sets arn"
}

output "waf_ipsets_cf_ipv6_arn" {
  value       = aws_wafv2_ip_set.blacklist_cf_ipv6.arn
  description = "IP sets arn"
}

```



### AWS ManagedRules
Here are the rules I use from ManagedRules provided by AWS.

| name | rule |
|:-----|:-----|
|[AWSManagedRulesCommonRuleSet](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs)|General rules, including those listed in OWASP, CVE, etc.|
|[AWSManagedRulesKnownBadInputsRuleSet](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-known-bad-inputs)|Detection of requests to discover/exploit vulnerabilities.|
|[AWSManagedRulesAmazonIpReputationList](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-amazon)|Inspects IPs that have been identified as bots by Amazon threat intelligence. _|
|[AWSManagedRulesAnonymousIpList](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-anonymous)|Inspects for a list of IPs known to anonymize client information, like TOR nodes, temporary proxies, and other masking services.|
|[AWSManagedRulesSQLiRuleSet](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-sql-db)|block request patterns associated with exploitation of SQL databases, like SQL injection.|
|[AWSManagedRulesLinuxRuleSet](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-linux-os)|block request patterns associated with the exploitation of vulnerabilities specific to Linux, including LFI attacks.|
|[AWSManagedRulesUnixRuleSet](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-posix-os)|block request patterns associated with the exploitation of vulnerabilities specific to POSIX and POSIX-like OS, including LFI attacks. |

The app doesn't use WordPress and also PHP, but if you so, you should use WordPress or PHP rulesets. If you are using Windows, I recommend you should choose a ruleset based on Windows OS, not Linux. Please refer to [this reference](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html) for all of these rulesets. The rulesets also have capacity limits, so please refer to the doc to choose the rules you need.

For your reference, I have included the code I have set up in Terraform.
The AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet are listed as representatives. Other rules are also set in the same way, specifying their names in the `rule.name` and `rule.priority`.

To prevent false positives, these rule sets are set to COUNT Action by overriding the rule setting that BLOCK them.


```json
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 10

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

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesCommonRuleSetMetric"
      sampled_requests_enabled   = true
    }
  }


  rule {
    name     = "AWSManagedRulesKnownBadInputsRuleSet"
    priority = 20

    override_action {
      count {}
    }

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

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesKnownBadInputsRuleSetMetric"
      sampled_requests_enabled   = true
    }
  }

```


### Attach to CloudFront and ALB & Setting monitoring option <a name="chap3"></a>
Finally, I will associate WAF with AWS resources and configure logging.

In this case, I will show you an example of associating with ALB and CloudFront.

Also, I have chosen to output the logs to S3. Other than output to S3, you can also use Kinesis, and CloudWatch Logs. However, the resource name of the log output destination must always be set to `aws-waf-logs-`.
Please be careful([document](https://docs.aws.amazon.com/waf/latest/developerguide/logging-destinations.html))

```json
resource "aws_s3_bucket" "bucket" {
  bucket = var.bucket
  tags = {
    Name = "${var.prefix}-${var.name}"
  }
}

resource "aws_wafv2_web_acl_logging_configuration" "api_server_waf_log" {
  log_destination_configs = [aws_s3_bucket.bucket.arn]
  resource_arn            = aws_wafv2_web_acl.this.arn
}

// ARN
output "waf_arn" {
  value       = aws_wafv2_web_acl.this.arn
  description = "IP sets arn"
}

output "s3_bucket_arn" {
  value       = aws_s3_bucket.bucket.arn
  description = "S3 bucket arn"
}
```

The main file calling the module is described below.

```json
module "waf_alb" {
  source        = "../../modules/waf_alb"
  prefix        = local.prefix
  name          = "waf-alb"
  metric_name   = "waf-alb"
  alb_ipsets_v4 = module.waf_ipsets.waf_ipsets_alb_ipv4_arn
  alb_ipsets_v6 = module.waf_ipsets.waf_ipsets_alb_ipv6_arn
  bucket = "aws-waf-logs-api-server-prd-blah-blah"
}

module "waf_ipsets" {
  source = "../../modules/waf_ipsets"
  prefix = local.prefix
  name   = "ip"
}

module "waf_cf" {
  source       = "../../modules/waf_cf"
  prefix       = local.prefix
  name         = "waf-teacher"
  metric_name  = "waf-teacher"
  cf_ipsets_v4 = module.waf_ipsets.waf_ipsets_cf_ipv4_arn
  cf_ipsets_v6 = module.waf_ipsets.waf_ipsets_cf_ipv6_arn
  bucket = "aws-waf-logs-teacher-prd-blah-blah"
}

resource "aws_wafv2_web_acl_association" "api_server_waf" {
  resource_arn = module.hoge_hoge_alb.alb_arn
  web_acl_arn  = module.waf_alb.waf_arn
}

# CloudFront associated with WAF(※Module is not listed here) 
module "cloudfront_waf" {
  source          = "../../modules/cloudfront_spa_cdn"
  prefix          = local.prefix
  name            = "spa-frontend"
  cloudfront_fqdn = local.hoge_teacher_web_frontend_fqdn
  zone_id         = module.route53_zone.zone_id
  web_acl_id      = module.waf_teacher.waf_arn
}

# ALB associated with WAF(※Module is not listed here)
module "hoge_hoge_alb" {
  source                         = "../../modules/alb"
  prefix                         = local.prefix
  name                           = "api-alb"
  vpc_id                         = module.main_vpc.vpc_id
  zone_id                        = module.route53_zone.zone_id
  alb_subnets                    = module.main_vpc.public_subnets
  alb_fqdn                       = local.hoge_study_api_alb_fqdn
  target_group_port              = local.hoge_study_api_server_container_port
  target_group_health_check_path = local.hoge_study_api_server_health_check_path
}
```


# In Conclusion
Congrats🎉 Complete the set up💡
Again, if you want to see full sample, please look at [the GitHub Gist](https://gist.github.com/masayoshi644/13e9d17efec808f05e98b057439edfc1).
Happy AWS WAF Life!