Skip to content

Commit fd696c7

Browse files
feat(terraform): add the Azure waf module (#14104)
1 parent 17e7bfe commit fd696c7

5 files changed

Lines changed: 660 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
locals {
2+
ip_allowlist_enabled = length(var.allowed_ip_cidrs) > 0
3+
rate_limit_exempt_enabled = length(var.rate_limit_exempt_ip_cidrs) > 0
4+
geo_restriction_enabled = length(var.geo_restriction_countries) > 0
5+
6+
managed_rule_sets = concat(
7+
[{ type = "OWASP", version = var.owasp_rule_set_version }],
8+
var.enable_bot_protection ? [{ type = "Microsoft_BotManagerRuleSet", version = var.bot_manager_rule_set_version }] : [],
9+
)
10+
11+
# The overrides arrive as a flat list but the provider nests them by rule set
12+
# and then by rule group, so regroup them once here.
13+
overrides_by_set = {
14+
for set_type in distinct([for o in var.managed_rule_overrides : o.rule_set_type]) :
15+
set_type => {
16+
for group_name in distinct([for o in var.managed_rule_overrides : o.rule_group_name if o.rule_set_type == set_type]) :
17+
group_name => [for o in var.managed_rule_overrides : o if o.rule_set_type == set_type && o.rule_group_name == group_name]
18+
}
19+
}
20+
}
21+
22+
resource "azurerm_web_application_firewall_policy" "this" {
23+
name = "${var.name}-waf"
24+
resource_group_name = var.resource_group_name
25+
location = var.location
26+
tags = var.tags
27+
28+
policy_settings {
29+
enabled = true
30+
mode = var.mode
31+
request_body_check = true
32+
max_request_body_size_in_kb = var.max_request_body_size_in_kb
33+
file_upload_limit_in_mb = var.file_upload_limit_in_mb
34+
}
35+
36+
managed_rules {
37+
dynamic "managed_rule_set" {
38+
for_each = local.managed_rule_sets
39+
content {
40+
type = managed_rule_set.value.type
41+
version = managed_rule_set.value.version
42+
43+
dynamic "rule_group_override" {
44+
for_each = try(local.overrides_by_set[managed_rule_set.value.type], {})
45+
content {
46+
rule_group_name = rule_group_override.key
47+
48+
dynamic "rule" {
49+
for_each = rule_group_override.value
50+
content {
51+
id = rule.value.rule_id
52+
action = rule.value.action
53+
enabled = rule.value.enabled
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
# Anything not on the allowlist is refused before the managed rules run.
63+
dynamic "custom_rules" {
64+
for_each = local.ip_allowlist_enabled ? [1] : []
65+
content {
66+
name = "BlockRequestsOutsideAllowedIPs"
67+
priority = 1
68+
rule_type = "MatchRule"
69+
action = "Block"
70+
71+
match_conditions {
72+
match_variables {
73+
variable_name = "RemoteAddr"
74+
}
75+
operator = "IPMatch"
76+
negation_condition = true
77+
match_values = var.allowed_ip_cidrs
78+
}
79+
}
80+
}
81+
82+
dynamic "custom_rules" {
83+
for_each = local.geo_restriction_enabled ? [1] : []
84+
content {
85+
name = "BlockRestrictedCountries"
86+
priority = 10
87+
rule_type = "MatchRule"
88+
action = "Block"
89+
90+
match_conditions {
91+
match_variables {
92+
variable_name = "RemoteAddr"
93+
}
94+
operator = "GeoMatch"
95+
match_values = var.geo_restriction_countries
96+
}
97+
}
98+
}
99+
100+
# Match conditions on a rule are combined with AND, so the negated exempt
101+
# list is what keeps the limit from applying to those addresses.
102+
custom_rules {
103+
name = "ApiRateLimit"
104+
priority = 20
105+
rule_type = "RateLimitRule"
106+
action = "Block"
107+
rate_limit_duration = "FiveMins"
108+
rate_limit_threshold = var.api_rate_limit_requests_per_5_minutes
109+
group_rate_limit_by = "ClientAddr"
110+
111+
match_conditions {
112+
match_variables {
113+
variable_name = "RequestUri"
114+
}
115+
operator = "BeginsWith"
116+
match_values = [var.api_path_prefix]
117+
}
118+
119+
dynamic "match_conditions" {
120+
for_each = local.rate_limit_exempt_enabled ? [1] : []
121+
content {
122+
match_variables {
123+
variable_name = "RemoteAddr"
124+
}
125+
operator = "IPMatch"
126+
negation_condition = true
127+
match_values = var.rate_limit_exempt_ip_cidrs
128+
}
129+
}
130+
}
131+
132+
custom_rules {
133+
name = "GlobalRateLimit"
134+
priority = 30
135+
rule_type = "RateLimitRule"
136+
action = "Block"
137+
rate_limit_duration = "FiveMins"
138+
rate_limit_threshold = var.rate_limit_requests_per_5_minutes
139+
group_rate_limit_by = "ClientAddr"
140+
141+
match_conditions {
142+
match_variables {
143+
variable_name = "RemoteAddr"
144+
}
145+
operator = "IPMatch"
146+
match_values = ["0.0.0.0/0", "::/0"]
147+
}
148+
149+
dynamic "match_conditions" {
150+
for_each = local.rate_limit_exempt_enabled ? [1] : []
151+
content {
152+
match_variables {
153+
variable_name = "RemoteAddr"
154+
}
155+
operator = "IPMatch"
156+
negation_condition = true
157+
match_values = var.rate_limit_exempt_ip_cidrs
158+
}
159+
}
160+
}
161+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
output "policy_id" {
2+
description = "Resource ID of the WAF policy. Attach it to an Application Gateway. Front Door uses a different resource, azurerm_cdn_frontdoor_firewall_policy, and cannot take this one."
3+
value = azurerm_web_application_firewall_policy.this.id
4+
}
5+
6+
output "policy_name" {
7+
description = "Name of the WAF policy"
8+
value = azurerm_web_application_firewall_policy.this.name
9+
}
10+
11+
# Unlike the AWS module there is no log group here. Azure emits WAF logs from
12+
# the Application Gateway the policy is attached to, so the diagnostic setting
13+
# belongs on that resource.
14+
output "mode" {
15+
description = "Whether the policy blocks matches or only logs them"
16+
value = var.mode
17+
}

0 commit comments

Comments
 (0)