DEV Community

Cover image for How to set up AWS WAF v2 with Terraform
Masayoshi Haruta for AWS Community Builders

Posted on • Updated on

How to set up AWS WAF v2 with Terraform

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

How to Set up

  1. Create a Web ACL
  2. Create RuleSets
  3. Attach to CloudFront and ALB & Setting monitoring option

Sample Codes

Here are sample codes. 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

Web ACL is a central resource.
WAF image

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

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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

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
}

Enter fullscreen mode Exit fullscreen mode

2. Create a Rulesets

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.

  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
    }
  }

Enter fullscreen mode Exit fullscreen mode

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.

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

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
  }
}

Enter fullscreen mode Exit fullscreen mode

Also, I prepare another module "waf_ipsets" so that I manage blacklist IP sets for utilizing the above rule.

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"
}

Enter fullscreen mode Exit fullscreen mode

AWS ManagedRules

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

name rule
AWSManagedRulesCommonRuleSet General rules, including those listed in OWASP, CVE, etc.
AWSManagedRulesKnownBadInputsRuleSet Detection of requests to discover/exploit vulnerabilities.
AWSManagedRulesAmazonIpReputationList Inspects IPs that have been identified as bots by Amazon threat intelligence. _
AWSManagedRulesAnonymousIpList Inspects for a list of IPs known to anonymize client information, like TOR nodes, temporary proxies, and other masking services.
AWSManagedRulesSQLiRuleSet block request patterns associated with exploitation of SQL databases, like SQL injection.
AWSManagedRulesLinuxRuleSet block request patterns associated with the exploitation of vulnerabilities specific to Linux, including LFI attacks.
AWSManagedRulesUnixRuleSet 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 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.

  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
    }
  }

Enter fullscreen mode Exit fullscreen mode

Attach to CloudFront and ALB & Setting monitoring option

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)

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"
}
Enter fullscreen mode Exit fullscreen mode

The main file calling the module is described below.

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
}
Enter fullscreen mode Exit fullscreen mode

In Conclusion

Congrats🎉 Complete the set up💡
Again, if you want to see full sample, please look at the GitHub Gist.
Happy AWS WAF Life!

Top comments (0)