From 88624ab4fcd7d589f3a39e874c40b1427aecd5c5 Mon Sep 17 00:00:00 2001 From: Marcin Cuber Date: Mon, 5 Oct 2020 12:07:15 +0100 Subject: [PATCH] Add support for IP sets and rate limiting (#15) --- .pre-commit-config.yaml | 2 +- README.md | 26 ++++--- examples/wafv2-ip-rules/main.tf | 122 ++++++++++++++++++++++++++++++++ main.tf | 90 +++++++++++++++++++++++ variables.tf | 10 +++ versions.tf | 2 +- 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 examples/wafv2-ip-rules/main.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d688e1b..5094823 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: args: ['--allow-missing-credentials'] - id: trailing-whitespace - repo: git://github.com/antonbabenko/pre-commit-terraform - rev: v1.31.0 + rev: v1.43.0 hooks: - id: terraform_fmt - id: terraform_docs diff --git a/README.md b/README.md index 80f7717..6562499 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,16 @@ Terraform module to configure WAF Web ACL V2 for Application Load Balancer or Cloudfront distribution. -Module supports all AWS managed rules defained in https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html. +Supported WAF v2 components: + +- Module supports all AWS managed rules defained in https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html. +- Associating WAFv2 ACL with one or more Application Load Balancers (ALB) +- Blocking IP Sets +- Rate limiting IPs ## Terraform versions -Terraform 0.12. Pin module version to `~> v1.0`. Submit pull-requests to `master` branch. +Terraform 0.12 and 0.13. Pin module version to `~> v1.0`. Submit pull-requests to `master` branch. ## Usage @@ -15,7 +20,7 @@ Please pin down version of this module to exact version. ```hcl module "waf" { source = "umotif-public/waf-webaclv2/aws" - version = "~> 1.4.0" + version = "~> 1.5.0" name_prefix = "test-waf-setup" alb_arn = module.alb.arn @@ -27,7 +32,7 @@ module "waf" { allow_default_action = true # set to allow if not specified visibility_config = { - metric_name = "test-waf-setup-waf-main-metrics" + metric_name = "test-waf-setup-waf-main-metrics" } rules = [ @@ -58,7 +63,7 @@ module "waf" { override_action = "count" visibility_config = { - metric_name = "AWSManagedRulesKnownBadInputsRuleSet-metric" + metric_name = "AWSManagedRulesKnownBadInputsRuleSet-metric" } managed_rule_group_statement = { @@ -96,7 +101,7 @@ module "waf" { provider "aws" { alias = "us-east" - version = "~> 2.68" + version = ">= 2.70" region = "us-east-1" } @@ -106,7 +111,7 @@ module "waf" { } source = "umotif-public/waf-webaclv2/aws" - version = "~> 1.4.0" + version = "~> 1.5.0" name_prefix = "test-waf-setup-cloudfront" scope = "CLOUDFRONT" @@ -129,6 +134,7 @@ Importantly, make sure that Amazon Kinesis Data Firehose is using a name startin * [WAF ACL](https://github.com/umotif-public/terraform-aws-waf-webaclv2/tree/master/examples/core) * [WAF ACL with configuration logging](https://github.com/umotif-public/terraform-aws-waf-webaclv2/tree/master/examples/wafv2-logging-configuration) +* [WAF ACL with ip rules](https://github.com/umotif-public/terraform-aws-waf-webaclv2/tree/master/examples/wafv2-ip-rules) ## Authors @@ -140,13 +146,13 @@ Module managed by [Marcin Cuber](https://github.com/marcincuber) [LinkedIn](http | Name | Version | |------|---------| | terraform | >= 0.12.6, < 0.14 | -| aws | >= 2.68, < 4.0 | +| aws | >= 2.70, < 4.0 | ## Providers | Name | Version | |------|---------| -| aws | >= 2.68, < 4.0 | +| aws | >= 2.70, < 4.0 | ## Inputs @@ -158,6 +164,8 @@ Module managed by [Marcin Cuber](https://github.com/marcincuber) [LinkedIn](http | create\_alb\_association | Whether to create alb association with WAF web acl | `bool` | `true` | no | | create\_logging\_configuration | Whether to create logging configuration in order start logging from a WAFv2 Web ACL to Amazon Kinesis Data Firehose. | `bool` | `false` | no | | enabled | Whether to create the resources. Set to `false` to prevent the module from creating any resources | `bool` | `true` | no | +| ip\_rate\_based\_rule | A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span | `any` | `null` | no | +| ip\_set\_rules | List of WAF ip set rules to detect web requests coming from particular IP addresses or address ranges. | `list` | `[]` | no | | log\_destination\_configs | The Amazon Kinesis Data Firehose Amazon Resource Name (ARNs) that you want to associate with the web ACL. Currently, only 1 ARN is supported. | `list(string)` | `[]` | no | | name\_prefix | Name prefix used to create resources. | `string` | n/a | yes | | redacted\_fields | The parts of the request that you want to keep out of the logs. Up to 100 `redacted_fields` blocks are supported. | `list` | `[]` | no | diff --git a/examples/wafv2-ip-rules/main.tf b/examples/wafv2-ip-rules/main.tf new file mode 100644 index 0000000..bab1a8e --- /dev/null +++ b/examples/wafv2-ip-rules/main.tf @@ -0,0 +1,122 @@ +provider "aws" { + region = "eu-west-1" +} + +##### +# IP set resources +##### +resource "aws_wafv2_ip_set" "block_ip_set" { + name = "generated-ips" + + scope = "REGIONAL" + ip_address_version = "IPV4" + + # generates a list of all /16s + addresses = formatlist("%s.0.0.0/16", range(0, 50)) +} + +resource "aws_wafv2_ip_set" "custom_ip_set" { + name = "custom-ip-set" + + scope = "REGIONAL" + ip_address_version = "IPV4" + + addresses = [ + "10.0.0.0/16", + "10.10.0.0/16" + ] +} + +##### +# Web Application Firewall configuration +##### +module "waf" { + source = "../.." + + name_prefix = "test-waf-setup" + + allow_default_action = true + + create_alb_association = false + + visibility_config = { + cloudwatch_metrics_enabled = false + metric_name = "test-waf-setup-waf-main-metrics" + sampled_requests_enabled = false + } + + rules = [ + { + name = "AWSManagedRulesCommonRuleSet-rule-1" + priority = "1" + + override_action = "none" + + visibility_config = { + cloudwatch_metrics_enabled = false + metric_name = "AWSManagedRulesCommonRuleSet-metric" + sampled_requests_enabled = false + } + + managed_rule_group_statement = { + name = "AWSManagedRulesCommonRuleSet" + vendor_name = "AWS" + excluded_rule = [ + "SizeRestrictions_QUERYSTRING", + "SizeRestrictions_BODY", + "GenericRFI_QUERYARGUMENTS" + ] + } + } + ] + + ip_set_rules = [ + { + name = "allow-custom-ip-set" + priority = 5 + # action = "count" # if not set, action defaults to allow + ip_set_reference_statement = { + arn = aws_wafv2_ip_set.custom_ip_set.arn + } + + visibility_config = { + cloudwatch_metrics_enabled = false + sampled_requests_enabled = false + } + }, + { + name = "block-ip-set" + priority = 6 + action = "block" + ip_set_reference_statement = { + arn = aws_wafv2_ip_set.block_ip_set.arn + } + + visibility_config = { + cloudwatch_metrics_enabled = false + metric_name = "test-waf-setup-waf-ip-set-block-metrics" + sampled_requests_enabled = false + } + } + ] + + ip_rate_based_rule = { + name = "ip-rate-limit" + priority = 2 + # action = "count" # if not set, action defaults to block + + rate_based_statement = { + limit = 100 + aggregate_key_type = "IP" + } + + visibility_config = { + cloudwatch_metrics_enabled = false + sampled_requests_enabled = false + } + } + + tags = { + "Environment" = "test" + } +} diff --git a/main.tf b/main.tf index c54cf33..ea7e101 100644 --- a/main.tf +++ b/main.tf @@ -65,6 +65,96 @@ resource "aws_wafv2_web_acl" "main" { } } + dynamic "rule" { + for_each = var.ip_set_rules + content { + name = lookup(rule.value, "name") + priority = lookup(rule.value, "priority") + + action { + dynamic "allow" { + for_each = length(lookup(rule.value, "action", {})) == 0 || lookup(rule.value, "action", {}) == "allow" ? [1] : [] + content {} + } + + dynamic "count" { + for_each = lookup(rule.value, "action", {}) == "count" ? [1] : [] + content {} + } + + dynamic "block" { + for_each = lookup(rule.value, "action", {}) == "block" ? [1] : [] + content {} + } + } + + statement { + dynamic "ip_set_reference_statement" { + for_each = length(lookup(rule.value, "ip_set_reference_statement", {})) == 0 ? [] : [lookup(rule.value, "ip_set_reference_statement", {})] + content { + arn = lookup(ip_set_reference_statement.value, "arn") + } + } + } + + dynamic "visibility_config" { + for_each = length(lookup(rule.value, "visibility_config")) == 0 ? [] : [lookup(rule.value, "visibility_config", {})] + content { + cloudwatch_metrics_enabled = lookup(visibility_config.value, "cloudwatch_metrics_enabled", true) + metric_name = lookup(visibility_config.value, "metric_name", "${var.name_prefix}-ip-rule-metric-name") + sampled_requests_enabled = lookup(visibility_config.value, "sampled_requests_enabled", true) + } + } + } + } + + dynamic "rule" { + for_each = var.ip_rate_based_rule != null ? [var.ip_rate_based_rule] : [] + content { + name = lookup(rule.value, "name") + priority = lookup(rule.value, "priority") + + action { + dynamic "count" { + for_each = lookup(rule.value, "action", {}) == "count" ? [1] : [] + content {} + } + + dynamic "block" { + for_each = length(lookup(rule.value, "action", {})) == 0 || lookup(rule.value, "action", {}) == "block" ? [1] : [] + content {} + } + } + + statement { + dynamic "rate_based_statement" { + for_each = length(lookup(rule.value, "rate_based_statement", {})) == 0 ? [] : [lookup(rule.value, "rate_based_statement", {})] + content { + limit = lookup(rate_based_statement.value, "limit") + aggregate_key_type = lookup(rate_based_statement.value, "aggregate_key_type", "IP") + + dynamic "forwarded_ip_config" { + for_each = length(lookup(rule.value, "forwarded_ip_config", {})) == 0 ? [] : [lookup(rule.value, "forwarded_ip_config", {})] + content { + fallback_behavior = lookup(forwarded_ip_config.value, "fallback_behavior") + header_name = lookup(forwarded_ip_config.value, "header_name") + } + } + } + } + } + + dynamic "visibility_config" { + for_each = length(lookup(rule.value, "visibility_config")) == 0 ? [] : [lookup(rule.value, "visibility_config", {})] + content { + cloudwatch_metrics_enabled = lookup(visibility_config.value, "cloudwatch_metrics_enabled", true) + metric_name = lookup(visibility_config.value, "metric_name", "${var.name_prefix}-ip-rate-based-rule-metric-name") + sampled_requests_enabled = lookup(visibility_config.value, "sampled_requests_enabled", true) + } + } + } + } + tags = var.tags dynamic "visibility_config" { diff --git a/variables.tf b/variables.tf index 4980d8a..2f1a827 100644 --- a/variables.tf +++ b/variables.tf @@ -32,6 +32,16 @@ variable "rules" { default = [] } +variable "ip_set_rules" { + description = "List of WAF ip set rules to detect web requests coming from particular IP addresses or address ranges." + default = [] +} + +variable "ip_rate_based_rule" { + description = "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span" + default = null +} + variable "visibility_config" { description = "Visibility config for WAFv2 web acl. https://www.terraform.io/docs/providers/aws/r/wafv2_web_acl.html#visibility-configuration" type = map(string) diff --git a/versions.tf b/versions.tf index 8bd9622..b850a3a 100644 --- a/versions.tf +++ b/versions.tf @@ -2,6 +2,6 @@ terraform { required_version = ">= 0.12.6, < 0.14" required_providers { - aws = ">= 2.68, < 4.0" + aws = ">= 2.70, < 4.0" } }