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_reputationwhile keepingsqli,xss, etc. - Add new rules: Define rules not in the template
What Doesn't Merge
- Nested fields: Cannot partially override
statementorrule_action_overrides - Array merging: Cannot append to
rule_action_overridesarray
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:
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: