Skip to content

WAF Web ACLs

Input map: wafv2_web_acl

Define AWS WAF v2 Web ACLs with support for templates, AWS managed rule groups, and custom rules.

Key Fields

Field Type Default Description
name string auto-generated Web ACL name (auto-generated from key if omitted)
description string null Web ACL description
scope string "REGIONAL" REGIONAL (ALB, API Gateway) or CLOUDFRONT
default_action string "allow" Default action for requests that don't match rules: allow or block
rule_set_template string null Reference to a pre-configured rule template (e.g., sapphire_managed_ruleset_v1)
rules map {} Custom rules or template overrides
custom_response_body map {} Custom response bodies for block actions
visibility_config object required CloudWatch metrics configuration
tags map(string) {} Resource tags

Using Templates

Templates provide pre-configured AWS managed rule groups with sensible defaults.

Reference a Template

wafv2_web_acl = {
  my-waf = {
    rule_set_template = "sapphire_managed_ruleset_v1"

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

This inherits all rules from the template without modification.

Override Template Rules

Override specific rules while keeping others from the template:

wafv2_web_acl = {
  my-waf = {
    rule_set_template = "sapphire_managed_ruleset_v1"

    rules = {
      # Override ip_reputation to use count instead of block
      ip_reputation = {
        name            = "AWSManagedRulesAmazonIpReputationList"
        priority        = 1
        override_action = "none"

        statement = {
          managed_rule_group_statement = {
            name        = "AWSManagedRulesAmazonIpReputationList"
            vendor_name = "AWS"

            rule_action_overrides = [
              { name = "AWSManagedIPReputationList", action = "count" }
            ]
          }
        }

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

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

Result: Your ip_reputation definition replaces the template's. All other template rules (anonymous_ip, sqli, xss, etc.) are preserved.

Remove Template Rules

Set a rule to null to exclude it from the template:

wafv2_web_acl = {
  my-waf = {
    rule_set_template = "sapphire_managed_ruleset_v1"

    rules = {
      windows_rule_set = null  # Exclude this rule
    }

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

Add Custom Rules

Add rules not in the template:

wafv2_web_acl = {
  my-waf = {
    rule_set_template = "sapphire_managed_ruleset_v1"

    rules = {
      # New custom rate limit rule
      rate-limit = {
        name     = "RateLimitRule"
        priority = 100

        action = {
          type = "block"
          block = {
            custom_response = {
              response_code = 429
              custom_response_body_key = "rate_limit_body"
            }
          }
        }

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

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

    custom_response_body = {
      rate_limit_body = {
        content      = "Too many requests. Please try again later."
        content_type = "TEXT_PLAIN"
      }
    }

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

Custom Rules (No Template)

Create Web ACLs without templates:

wafv2_web_acl = {
  custom-waf = {
    description    = "Custom WAF without template"
    default_action = "allow"

    rules = {
      geo-block = {
        name     = "GeoBlockRule"
        priority = 1

        action = {
          type = "block"
        }

        statement = {
          geo_match_statement = {
            country_codes = ["CN", "RU", "KP"]
          }
        }

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

      ip-whitelist = {
        name     = "IPWhitelistRule"
        priority = 2

        action = {
          type = "allow"
        }

        statement = {
          ip_set_reference_statement = {
            arn = "arn:aws:wafv2:us-west-2:123456789012:regional/ipset/whitelist/abc123"
          }
        }

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

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

Merge Behavior

Important: Terraform's merge() performs shallow merge only.

When overriding a rule from a template:

# Template has:
ip_reputation = {
  name            = "..."
  priority        = 1
  override_action = "none"
  statement       = { ... }
  visibility_config = { ... }
}

# You override with:
ip_reputation = {
  priority = 2  # Just want to change priority
}

# Result: ONLY priority exists
# Lost: name, override_action, statement, visibility_config

Solution: Always provide the COMPLETE rule definition when overriding.

What Merges

  • Rule-level merge: Override ip_reputation while keeping sqli, xss, etc.
  • Add new rules: Define rules not in the template

What Doesn't Merge

  • Nested fields: Cannot partially override statement or rule_action_overrides
  • Array merging: Cannot append to rule_action_overrides array

To modify nested fields, copy the entire rule from the template and make your changes.

Current templates: - managed_ruleset_v1 – Comprehensive protection with 9 rules (6 AWS managed rule groups + 3 custom rules)

Creating Custom Templates

To create a new template, edit src/modules/wafv2_web_acl/locals.templates.tf:

locals {
  wafv2_web_acl_rule_sets = {
    # Existing templates...

    # New custom template
    my_custom_template_v1 = {
      rules = {
        my_rule = {
          name     = "CustomRule"
          priority = 1

          action = {
            type = "block"
          }

          statement = {
            # ... rule definition ...
          }

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

Then reference it:

wafv2_web_acl = {
  my-waf = {
    rule_set_template = "my_custom_template_v1"
    # ...
  }
}

Template Versioning

Templates use semantic versioning (e.g., v1, v2) to allow:

  • Breaking changes – Create new version (e.g., sapphire_managed_ruleset_v2)
  • Backward compatibility – Existing environments continue using v1
  • Gradual migration – Update environments individually to new version

AWS Documentation

For complete rule documentation: