├── .terraform-docs.yml ├── .gitignore ├── versions.tf ├── outputs.tf ├── .markdownlintrc ├── examples └── simple │ ├── variables.tf │ └── main.tf ├── .github └── workflows │ └── validate.yml ├── .pre-commit-config.yaml ├── renovate.json ├── LICENSE ├── variables.tf ├── scripts └── delete-web-acl ├── main.tf └── README.md /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | html: false 3 | anchor: false 4 | formatter: "markdown table" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | terraform.tfstate.*.backup 6 | vendor 7 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = ">= 3.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "waf_acl_id" { 2 | description = "WAF ACL ID generated by the module" 3 | value = aws_wafregional_web_acl.wafacl.id 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "first-header-h1": false, 4 | "first-line-h1": false, 5 | "line_length": false, 6 | "no-multiple-blanks": false 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple/variables.tf: -------------------------------------------------------------------------------- 1 | variable "waf_acl_name" { 2 | type = string 3 | } 4 | 5 | variable "waf_acl_metric_name" { 6 | type = string 7 | } 8 | 9 | variable "vpc_azs" { 10 | type = list(string) 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate-tf 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | validate-tf: 13 | uses: trussworks/shared-actions/.github/workflows/validate-tf.yml@main 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-json 6 | - id: check-merge-conflict 7 | - id: check-yaml 8 | - id: detect-private-key 9 | - id: pretty-format-json 10 | args: 11 | - --autofix 12 | - id: trailing-whitespace 13 | - id: check-symlinks 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | 17 | - repo: https://github.com/executablebooks/mdformat 18 | rev: 0.7.16 19 | hooks: 20 | - id: mdformat 21 | additional_dependencies: 22 | - mdformat-gfm 23 | - mdformat-toc 24 | # mdformat fights with terraform_docs 25 | exclude: README.m(ark)?d(own)? 26 | 27 | - repo: https://github.com/igorshubovych/markdownlint-cli 28 | rev: v0.33.0 29 | hooks: 30 | - id: markdownlint 31 | 32 | - repo: https://github.com/detailyang/pre-commit-shell 33 | rev: 1.0.5 34 | hooks: 35 | - id: shell-lint 36 | 37 | - repo: https://github.com/antonbabenko/pre-commit-terraform 38 | rev: v1.77.1 39 | hooks: 40 | - id: terraform_fmt 41 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableDependencyDashboard" 5 | ], 6 | "labels": [ 7 | "dependencies" 8 | ], 9 | "packageRules": [ 10 | { 11 | "automerge": true, 12 | "description": "Automerge all updates except major versions", 13 | "matchUpdateTypes": [ 14 | "patch", 15 | "pin", 16 | "digest", 17 | "minor" 18 | ] 19 | }, 20 | { 21 | "description": "Tag the waddlers Github Team for major updates", 22 | "matchUpdateTypes": [ 23 | "major" 24 | ], 25 | "reviewers": [ 26 | "team:waddlers" 27 | ] 28 | }, 29 | { 30 | "automerge": true, 31 | "description": "Group minor and patch updates into a single PR", 32 | "groupName": "dependencies", 33 | "managers": [ 34 | "terraform", 35 | "pre-commit", 36 | "dockerfile", 37 | "github-actions" 38 | ], 39 | "matchUpdateTypes": [ 40 | "minor", 41 | "patch" 42 | ] 43 | } 44 | ], 45 | "schedule": [ 46 | "every weekend" 47 | ], 48 | "separateMinorPatch": true, 49 | "timezone": "America/Los_Angeles" 50 | } 51 | -------------------------------------------------------------------------------- /examples/simple/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # WAF 3 | # 4 | 5 | resource "aws_wafregional_rate_based_rule" "ipratelimit" { 6 | name = "app-global-ip-rate-limit" 7 | metric_name = "wafAppGlobalIpRateLimit" 8 | rate_key = "IP" 9 | rate_limit = 2000 10 | } 11 | 12 | module "waf" { 13 | source = "../../" 14 | 15 | alb_arn = aws_lb.main.arn 16 | associate_alb = true 17 | 18 | blocked_path_prefixes = ["/admin", "/password"] 19 | allowed_hosts = ["apples", "oranges"] 20 | 21 | rate_based_rules = [aws_wafregional_rate_based_rule.ipratelimit.id] 22 | 23 | web_acl_name = var.waf_acl_name 24 | web_acl_metric_name = var.waf_acl_metric_name 25 | } 26 | 27 | # 28 | # ALB 29 | # 30 | 31 | resource "aws_lb" "main" { 32 | internal = true 33 | load_balancer_type = "application" 34 | subnets = module.vpc.private_subnets 35 | 36 | tags = { 37 | Automation = "Terraform" 38 | } 39 | } 40 | 41 | # 42 | # VPC 43 | # 44 | 45 | module "vpc" { 46 | source = "terraform-aws-modules/vpc/aws" 47 | version = "~> 2" 48 | cidr = "10.0.0.0/16" 49 | azs = var.vpc_azs 50 | private_subnets = [ 51 | "10.0.1.0/24", 52 | "10.0.2.0/24" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, TrussWorks, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "alb_arn" { 2 | description = "ARN of the Application Load Balancer (ALB) to be associated with the Web Application Firewall (WAF) Access Control List (ACL)." 3 | type = string 4 | } 5 | 6 | variable "allowed_hosts" { 7 | description = "The list of allowed host names as specified in HOST header." 8 | type = list(string) 9 | } 10 | 11 | variable "associate_alb" { 12 | description = "Whether to associate an Application Load Balancer (ALB) with an Web Application Firewall (WAF) Access Control List (ACL)." 13 | type = bool 14 | default = false 15 | } 16 | 17 | variable "blocked_path_prefixes" { 18 | description = "The list of URI path prefixes to block using the WAF." 19 | type = list(string) 20 | default = [] 21 | } 22 | 23 | variable "ip_sets" { 24 | description = "List of sets of IP addresses to block." 25 | type = list(string) 26 | default = [] 27 | } 28 | 29 | variable "rate_based_rules" { 30 | description = "List of IDs of Rate-Based Rules to add to this WAF. Only use this variable for rate-based rules. Use the \"rules\" variable for regular rules." 31 | type = list(string) 32 | default = [] 33 | } 34 | 35 | variable "rules" { 36 | description = "List of IDs of Rules to add to this WAF. Only use this variable for regular rules. Use the \"rate_based_rules\" variable for rate-based rules." 37 | type = list(string) 38 | default = [] 39 | } 40 | 41 | variable "wafregional_rule_f5_id" { 42 | description = "The ID of the F5 Rule Group to use for the WAF for the ALB. Find the id with \"aws waf-regional list-subscribed-rule-groups\"." 43 | type = string 44 | default = "" 45 | } 46 | 47 | variable "web_acl_name" { 48 | description = "Name of the Web ACL" 49 | type = string 50 | } 51 | 52 | variable "web_acl_metric_name" { 53 | description = "Metric name of the Web ACL" 54 | type = string 55 | } 56 | -------------------------------------------------------------------------------- /scripts/delete-web-acl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -eu -o pipefail 4 | 5 | # 6 | # delete-web-acl 7 | # 8 | # delete-web-acl deletes a Web ACL and first clears the associations and removes all the rules from the acl. 9 | # 10 | 11 | function usage() { 12 | echo "Usage: ${0##*/} " 13 | exit 1 14 | } 15 | 16 | if [[ $# -ne 1 ]]; then 17 | usage 18 | fi 19 | 20 | webAclId=$1 21 | 22 | if ! command -v aws &> /dev/null; then 23 | echo "error: aws not installed, please install AWS CLi" 1>&2 24 | exit 1 25 | fi 26 | 27 | if ! command -v jq &> /dev/null; then 28 | echo "error: jq not installed, please install JQ" 1>&2 29 | exit 1 30 | fi 31 | 32 | echo "Deleting ACL ${webAclId}" 33 | 34 | echo "Current Web ACL" 35 | webAcl=$(aws waf-regional get-web-acl --web-acl-id "${webAclId}" | jq -cr .) 36 | echo "${webAcl}" | jq . 37 | 38 | # shellcheck disable=SC2207 39 | resources=($(aws waf-regional list-resources-for-web-acl --web-acl-id "${webAclId}" | jq -r ".ResourceArns[]")) 40 | echo "Current Associations" 41 | echo "${resources[@]:-None}" | tr ' ' '\n' 42 | echo "" 43 | 44 | for resource in "${resources[@]}"; do 45 | echo "Disassociating resource ${resource}" 46 | aws waf-regional disassociate-web-acl --resource-arn "${resource}" 47 | done 48 | 49 | echo "" 50 | 51 | # shellcheck disable=SC2207 52 | rules=($(aws waf-regional get-web-acl --web-acl-id "${webAclId}" | jq -c ".WebACL.Rules[]" )) 53 | echo "Current Rules" 54 | echo "${rules[@]:-None}" | tr ' ' '\n' 55 | echo "" 56 | 57 | for rule in "${rules[@]}"; do 58 | echo "Removing rule ${rule}" 59 | id=$(echo "${rule}" | jq -r .RuleId) 60 | priority=$(echo "${rule}" | jq -r .Priority) 61 | actionType=$(echo "${rule}" | jq -r .Action.Type) 62 | type=$(echo "${rule}" | jq -r .Type) 63 | 64 | echo "Using change token" 65 | CHANGE_TOKEN=$(aws waf-regional get-change-token | jq -r .ChangeToken) 66 | echo "${CHANGE_TOKEN}" 67 | 68 | # shellcheck disable=SC2026,SC1083 69 | updates=Action='"'DELETE'"',ActivatedRule={Priority=${priority},RuleId='"'${id}'"',Action={Type='"'${actionType}'"'},Type='"'${type}'"'} 70 | aws waf-regional update-web-acl --web-acl-id "${webAclId}" --change-token "${CHANGE_TOKEN}" --updates "${updates}" > /dev/null 71 | 72 | done 73 | 74 | echo "Using change token" 75 | CHANGE_TOKEN=$(aws waf-regional get-change-token | jq -r .ChangeToken) 76 | echo "${CHANGE_TOKEN}" 77 | 78 | echo "Deleting web acl" 79 | aws waf-regional delete-web-acl --web-acl-id "${webAclId}" --change-token "${CHANGE_TOKEN}" > /dev/null 80 | 81 | echo "Done" 82 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_wafregional_rule" "ips" { 2 | count = length(var.ip_sets) 3 | 4 | name = format("%s-ips-%d", var.web_acl_name, count.index) 5 | metric_name = format("%sIPs%d", var.web_acl_metric_name, count.index) 6 | 7 | predicate { 8 | data_id = var.ip_sets[count.index] 9 | negated = false 10 | type = "IPMatch" 11 | } 12 | 13 | lifecycle { 14 | create_before_destroy = true 15 | } 16 | } 17 | 18 | resource "aws_wafregional_byte_match_set" "allowed_hosts" { 19 | name = format("%s-allowed-hosts", var.web_acl_name) 20 | 21 | dynamic "byte_match_tuples" { 22 | for_each = var.allowed_hosts 23 | content { 24 | 25 | # Even though the AWS Console web UI suggests a capitalized "host" data, 26 | # the data should be lower case as the AWS API will silently lowercase anyway. 27 | field_to_match { 28 | type = "HEADER" 29 | data = "host" 30 | } 31 | 32 | target_string = byte_match_tuples.value 33 | 34 | # See ByteMatchTuple for possible variable options. 35 | # See https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchTuple.html#WAF-Type-ByteMatchTuple-PositionalConstraint 36 | positional_constraint = "EXACTLY" 37 | 38 | # Use COMPRESS_WHITE_SPACE to prevent sneaking around regex filter with 39 | # extra or non-standard whitespace 40 | # See https://docs.aws.amazon.com/sdk-for-go/api/service/waf/#RegexMatchTuple 41 | text_transformation = "COMPRESS_WHITE_SPACE" 42 | } 43 | } 44 | } 45 | 46 | resource "aws_wafregional_rule" "allowed_hosts" { 47 | name = format("%s-allowed-hosts", var.web_acl_name) 48 | metric_name = format("%sAllowedHosts", var.web_acl_metric_name) 49 | 50 | predicate { 51 | type = "ByteMatch" 52 | data_id = aws_wafregional_byte_match_set.allowed_hosts.id 53 | negated = true 54 | } 55 | } 56 | 57 | resource "aws_wafregional_byte_match_set" "blocked_path_prefixes" { 58 | name = format("%s-blocked-path-prefixes", var.web_acl_name) 59 | 60 | dynamic "byte_match_tuples" { 61 | for_each = var.blocked_path_prefixes 62 | content { 63 | field_to_match { 64 | type = "URI" 65 | } 66 | 67 | target_string = byte_match_tuples.value 68 | 69 | # See ByteMatchTuple for possible variable options. 70 | # See https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchTuple.html#WAF-Type-ByteMatchTuple-PositionalConstraint 71 | positional_constraint = "STARTS_WITH" 72 | 73 | # Use COMPRESS_WHITE_SPACE to prevent sneaking around regex filter with 74 | # extra or non-standard whitespace 75 | # See https://docs.aws.amazon.com/sdk-for-go/api/service/waf/#RegexMatchTuple 76 | text_transformation = "COMPRESS_WHITE_SPACE" 77 | } 78 | } 79 | } 80 | 81 | resource "aws_wafregional_rule" "blocked_path_prefixes" { 82 | name = format("%s-blocked-path-prefixes", var.web_acl_name) 83 | metric_name = format("%sBlockedPathPrefixes", var.web_acl_metric_name) 84 | 85 | predicate { 86 | type = "ByteMatch" 87 | data_id = aws_wafregional_byte_match_set.blocked_path_prefixes.id 88 | negated = false 89 | } 90 | } 91 | 92 | resource "aws_wafregional_web_acl" "wafacl" { 93 | name = var.web_acl_name 94 | metric_name = var.web_acl_metric_name 95 | 96 | default_action { 97 | type = "ALLOW" 98 | } 99 | 100 | dynamic "rule" { 101 | for_each = aws_wafregional_rule.ips.*.id 102 | content { 103 | type = "REGULAR" 104 | rule_id = rule.value 105 | priority = 1 + rule.key 106 | 107 | action { 108 | type = "BLOCK" 109 | } 110 | } 111 | } 112 | 113 | rule { 114 | type = "REGULAR" 115 | rule_id = aws_wafregional_rule.allowed_hosts.id 116 | priority = 1 + length(aws_wafregional_rule.ips.*.id) 117 | 118 | action { 119 | type = "BLOCK" 120 | } 121 | } 122 | 123 | rule { 124 | type = "REGULAR" 125 | rule_id = aws_wafregional_rule.blocked_path_prefixes.id 126 | priority = 1 + length(aws_wafregional_rule.ips.*.id) + 1 127 | 128 | action { 129 | type = "BLOCK" 130 | } 131 | } 132 | 133 | dynamic "rule" { 134 | for_each = var.rate_based_rules 135 | content { 136 | type = "RATE_BASED" 137 | rule_id = rule.value 138 | priority = 1 + length(aws_wafregional_rule.ips.*.id) + 1 + 1 + rule.key 139 | 140 | action { 141 | type = "BLOCK" 142 | } 143 | } 144 | } 145 | 146 | dynamic "rule" { 147 | for_each = length(var.wafregional_rule_f5_id) > 0 ? [var.wafregional_rule_f5_id] : [] 148 | content { 149 | type = "GROUP" 150 | rule_id = rule.value 151 | priority = 1 + length(aws_wafregional_rule.ips.*.id) + 1 + 1 + length(var.rate_based_rules) + rule.key 152 | 153 | override_action { 154 | type = "NONE" 155 | } 156 | } 157 | } 158 | 159 | dynamic "rule" { 160 | for_each = var.rules 161 | content { 162 | type = "REGULAR" 163 | rule_id = rule.value 164 | priority = 1 + length(aws_wafregional_rule.ips.*.id) + 1 + 1 + length(var.rate_based_rules) + (length(var.wafregional_rule_f5_id) > 0 ? 1 : 0) + rule.key 165 | 166 | action { 167 | type = "BLOCK" 168 | } 169 | } 170 | } 171 | 172 | lifecycle { 173 | create_before_destroy = true 174 | } 175 | 176 | } 177 | 178 | resource "aws_wafregional_web_acl_association" "main" { 179 | count = var.associate_alb ? 1 : 0 180 | resource_arn = var.alb_arn 181 | web_acl_id = aws_wafregional_web_acl.wafacl.id 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Creates a WAF and associates it with an Application Load Balancer (ALB) 2 | 3 | Creates the following resources: 4 | 5 | - Web Application Firewall (WAF) 6 | - Links F5-managed OWASP rules for WAF to block common attacks 7 | - Creates rule for WAF to block requests by source IP Address (**Note**: the list of blocked IPs are not managed by this module) 8 | - Creates rule for WAF to block requests by path (as found in URI) 9 | - Creates rule for WAF to allow requests by host (as found in HTTP Header) 10 | - Attaches WAF to Application Load Balancer (ALB) 11 | 12 | 13 | ## Usage 14 | 15 | ```hcl 16 | resource "aws_wafregional_rate_based_rule" "ipratelimit" { 17 | name = "app-global-ip-rate-limit" 18 | metric_name = "wafAppGlobalIpRateLimit" 19 | rate_key = "IP" 20 | rate_limit = 2000 21 | } 22 | 23 | module "waf" { 24 | source = "trussworks/waf/aws" 25 | 26 | alb_arn = module.alb_web_containers.alb_arn 27 | associate_alb = true 28 | allowed_hosts = [var.domain_name] 29 | blocked_path_prefixes = var.blocked_path_prefixes 30 | ip_sets = var.ip_sets 31 | rate_based_rules = [aws_wafregional_rate_based_rule.ipratelimit.id] 32 | rules = var.rules 33 | wafregional_rule_f5_id = var.wafregional_rule_id 34 | web_acl_metric_name = "wafAppHelloWorld" 35 | web_acl_name = "app-hello-world" 36 | } 37 | ``` 38 | 39 | 40 | ## Requirements 41 | 42 | | Name | Version | 43 | |------|---------| 44 | | terraform | >= 1.0 | 45 | | aws | >= 3.0 | 46 | 47 | ## Providers 48 | 49 | | Name | Version | 50 | |------|---------| 51 | | aws | >= 3.0 | 52 | 53 | ## Modules 54 | 55 | No modules. 56 | 57 | ## Resources 58 | 59 | | Name | Type | 60 | |------|------| 61 | | [aws_wafregional_byte_match_set.allowed_hosts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_byte_match_set) | resource | 62 | | [aws_wafregional_byte_match_set.blocked_path_prefixes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_byte_match_set) | resource | 63 | | [aws_wafregional_rule.allowed_hosts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_rule) | resource | 64 | | [aws_wafregional_rule.blocked_path_prefixes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_rule) | resource | 65 | | [aws_wafregional_rule.ips](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_rule) | resource | 66 | | [aws_wafregional_web_acl.wafacl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_web_acl) | resource | 67 | | [aws_wafregional_web_acl_association.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafregional_web_acl_association) | resource | 68 | 69 | ## Inputs 70 | 71 | | Name | Description | Type | Default | Required | 72 | |------|-------------|------|---------|:--------:| 73 | | alb\_arn | ARN of the Application Load Balancer (ALB) to be associated with the Web Application Firewall (WAF) Access Control List (ACL). | `string` | n/a | yes | 74 | | allowed\_hosts | The list of allowed host names as specified in HOST header. | `list(string)` | n/a | yes | 75 | | associate\_alb | Whether to associate an Application Load Balancer (ALB) with an Web Application Firewall (WAF) Access Control List (ACL). | `bool` | `false` | no | 76 | | blocked\_path\_prefixes | The list of URI path prefixes to block using the WAF. | `list(string)` | `[]` | no | 77 | | ip\_sets | List of sets of IP addresses to block. | `list(string)` | `[]` | no | 78 | | rate\_based\_rules | List of IDs of Rate-Based Rules to add to this WAF. Only use this variable for rate-based rules. Use the "rules" variable for regular rules. | `list(string)` | `[]` | no | 79 | | rules | List of IDs of Rules to add to this WAF. Only use this variable for regular rules. Use the "rate\_based\_rules" variable for rate-based rules. | `list(string)` | `[]` | no | 80 | | wafregional\_rule\_f5\_id | The ID of the F5 Rule Group to use for the WAF for the ALB. Find the id with "aws waf-regional list-subscribed-rule-groups". | `string` | `""` | no | 81 | | web\_acl\_metric\_name | Metric name of the Web ACL | `string` | n/a | yes | 82 | | web\_acl\_name | Name of the Web ACL | `string` | n/a | yes | 83 | 84 | ## Outputs 85 | 86 | | Name | Description | 87 | |------|-------------| 88 | | waf\_acl\_id | WAF ACL ID generated by the module | 89 | 90 | 91 | ## Upgrade Path 92 | 93 | ### 2.0.0 to 2.1.0 94 | 95 | Version `2.1.0` removes the `ip_rate_limit` variables and replaces it with a `rate_based_rules` variable. The new variable accepts a list of `aws_wafregional_rate_based_rule` ids. This variables allows the Web ACL to use a global rate limit or provide custom rate limits for different paths. 96 | 97 | ```hcl 98 | resource "aws_wafregional_rate_based_rule" "ipratelimit" { 99 | name = "app-global-ip-rate-limit" 100 | metric_name = "wafAppGlobalIpRateLimit" 101 | 102 | rate_key = "IP" 103 | rate_limit = 2000 104 | } 105 | ``` 106 | 107 | Use `terraform state mv` to externalize the rate limit rule, e.g., `terraform state mv FOO.BAR.aws_wafregional_rate_based_rule.ipratelimit Foo.aws_wafregional_rate_based_rule.ipratelimit`. 108 | 109 | Version `2.1.0` removes the `regex_host_allow_pattern_strings` variable and replaces it with a required `allowed_hosts` variable. That variable now takes a list of fully qualified domain names rather than regex strings. If you ALB supports multiple domain names, each domain name will need to be added to the list. 110 | 111 | Version `2.1.0` removes the `regex_path_disallow_pattern_strings` variable and replaces it with an optional `blocked_path_prefixes` variable. That variable now takes a list of URI path prefixes rather than regex strings. 112 | 113 | Version `2.1.0` adds the `rules` variable which accepts a list of rule ids, which will be appended to the internally-managed rules. 114 | 115 | ### 1.3.0 to 2.0.0 116 | 117 | Version `2.0.0` removes the `environment` variable and adds `web_acl_metric_name` and `web_acl_name` variables to provide more control over naming. AWS WAF rules will be prefixed by the `web_acl_name` of their associated Web ACL to provide for easy visual sorting. 118 | 119 | Version `2.0.0` replaces the `ip_set` variable with a `ip_sets` list variable, which accepts a list of `aws_wafregional_ipset` ids. This variable allows the Web ACL to pull from multiple lists of blocked ip addresses, such that you can combine a global blocked list, and application-specific lists. For example: `ip_sets = [resource.aws_wafregional_ipset.global.id, resource.aws_wafregional_ipset.helloworld.id]`. 120 | 121 | During the initial upgrade to `2.0.0`, and if you add additional dynamic rules, you'll need to delete your web ACLs, as terraform cannot properly handle peer dependencies between Rules and Web ACLs. For convenience, you can use the `delete-web-acl` script in the scripts folder. For example: `scripts/delete-web-acl WEB_ACL_ID`. Once the Web ACL is deleted use terraform apply to recreate the Web ACL and associate with your resources as you had before. Deleting a Web ACL does not delete any associated resources, such as Application Load Balancers; however, it will leave the resources temporarily unprotected. 122 | 123 | ### 1.2.2 to 1.3.0 124 | 125 | Version `1.3.0` removes the `aws_wafregional_ipset` `ips` resource from this module and requires a `ip_set` variable that accepts the id of an externally managed `aws_wafregional_ipset`. This allows for a common IP Set to be used by multiple Web Application Firewalls. If your IP Set does not contain any IP addresses, then no IP addresses are blocked. For example: 126 | 127 | ```hcl 128 | resource "aws_wafregional_ipset" "global" { 129 | name = "app-global-blocked-ips" 130 | 131 | ip_set_descriptor { 132 | type = "IPV4" 133 | value = "1.2.3.4/32" 134 | } 135 | 136 | } 137 | ``` 138 | 139 | Use `terraform state mv` to externalize the IP Set, e.g., `terraform state mv FOO.BAR.aws_wafregional_ipset.ips Foo.aws_wafregional_ipset.ips`. 140 | 141 | ## Developer Setup 142 | 143 | Install dependencies (macOS) 144 | 145 | ```shell 146 | brew install pre-commit go terraform terraform-docs 147 | ``` 148 | --------------------------------------------------------------------------------