diff --git a/.gitignore b/.gitignore index a7c7693..71655d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.tfstate.* crash.log *.tfvars* +!terraform.tfvars override.tf override.tf.json *_override.tf diff --git a/README.md b/README.md index e66d32e..a89196b 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,10 @@ This project creates and manages the RSD FrontDoor CDN. | [key\_vault\_access\_ipv4](#input\_key\_vault\_access\_ipv4) | List of IPv4 Addresses that are permitted to access the Key Vault | `list(string)` | n/a | yes | | [tags](#input\_tags) | Tags to be applied to all resources | `map(string)` | `{}` | no | | [tfvars\_filename](#input\_tfvars\_filename) | tfvars filename. This file is uploaded and stored encrypted within Key Vault, to ensure that the latest tfvars are stored in a shared place. | `string` | n/a | yes | +| [waf\_custom\_rules](#input\_waf\_custom\_rules) | Map of all Custom rules you want to apply to the WAF |
map(object({
priority : number,
action : string
match_conditions : map(object({
match_variable : string,
match_values : optional(list(string), []),
operator : optional(string, "Any"),
selector : optional(string, null),
negation_condition : optional(bool, false),
}))
}))
| `{}` | no | | [waf\_enable\_bot\_protection](#input\_waf\_enable\_bot\_protection) | Enable Bot Protection rules in the WAF Policy | `bool` | `true` | no | | [waf\_enable\_rate\_limiting](#input\_waf\_enable\_rate\_limiting) | Enable a Rate Limit rule in the WAF Policy | `bool` | `true` | no | +| [waf\_managed\_rulesets](#input\_waf\_managed\_rulesets) | A map of managed rule sets and their group-level overrides. The key is the rule set type and version, e.g., 'Microsoft\_DefaultRuleSet\_2.1'. |
map(object({
action = optional(string, "Block")
overrides = optional(map(object({
disabled_rules = optional(list(string), [])
exclusions = optional(list(object({
match_variable = string
operator = string
selector = string
})), [])
})), {})
}))
| `{}` | no | | [waf\_mode](#input\_waf\_mode) | Set whether the WAF is in Detection or Prevention mode | `string` | `"Detection"` | no | | [waf\_rate\_limiting\_bypass\_ip\_list](#input\_waf\_rate\_limiting\_bypass\_ip\_list) | List of IPv4 Addresses that should bypass the Rate Limit rules | `list(string)` | `[]` | no | | [waf\_rate\_limiting\_duration\_in\_minutes](#input\_waf\_rate\_limiting\_duration\_in\_minutes) | Number of minutes to block an IP address when it exceeds rate limit | `number` | `5` | no | diff --git a/locals.tf b/locals.tf index a4e1b3b..aaef360 100644 --- a/locals.tf +++ b/locals.tf @@ -56,7 +56,8 @@ locals { waf_rate_limiting_duration_in_minutes = var.waf_rate_limiting_duration_in_minutes waf_rate_limiting_threshold = var.waf_rate_limiting_threshold waf_rate_limiting_bypass_ip_list = var.waf_rate_limiting_bypass_ip_list - + waf_custom_rules = var.waf_custom_rules + waf_managed_rulesets = var.waf_managed_rulesets enable_custom_reroute_ruleset = var.enable_custom_reroute_ruleset complete_dotnet_ruby_migration_paths_development = { diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..980e53d --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1,41 @@ +# Disable rules at the Edge, these will be handled by the App Gateway WAF as +# AFD has a 100-exclusion limit due to Global Propagation +waf_managed_rulesets = { + "Microsoft_DefaultRuleSet_2.1" = { + action = "Block" + overrides = { + "SQLI" = { + disabled_rules = [ + "942100", "942110", "942150", "942180", "942200", "942210", + "942230", "942260", "942310", "942340", "942360", "942370", "942380", + "942390", "942400", "942410", "942430", "942440", "942450", "942480" + ] + }, + "XSS" = { + disabled_rules = ["941100", "941120", "941150", "941330"] + }, + "RFI" = { + disabled_rules = ["931130"] + }, + "MS-ThreatIntel-SQLI" = { + disabled_rules = ["99031001", "99031002", "99031003", "99031004"] + }, + "PHP" = { + disabled_rules = ["933160", "933210"] + }, + "PROTOCOL-ENFORCEMENT" = { + disabled_rules = ["920230", "920300", "920320", "920440"] + }, + "PROTOCOL-ATTACK" = { + disabled_rules = ["921110"] + }, + "RCE" = { + disabled_rules = ["932100", "932105", "932110", "932115", "932150"] + } + } + }, + "Microsoft_BotManagerRuleSet_1.1" = { + action = "Block" + overrides = {} + } +} diff --git a/variables.tf b/variables.tf index 6158d82..b698c58 100644 --- a/variables.tf +++ b/variables.tf @@ -190,6 +190,38 @@ variable "waf_rate_limiting_bypass_ip_list" { default = [] } +variable "waf_custom_rules" { + description = "Map of all Custom rules you want to apply to the WAF" + type = map(object({ + priority : number, + action : string + match_conditions : map(object({ + match_variable : string, + match_values : optional(list(string), []), + operator : optional(string, "Any"), + selector : optional(string, null), + negation_condition : optional(bool, false), + })) + })) + default = {} +} + +variable "waf_managed_rulesets" { + description = "A map of managed rule sets and their group-level overrides. The key is the rule set type and version, e.g., 'Microsoft_DefaultRuleSet_2.1'." + type = map(object({ + action = optional(string, "Block") + overrides = optional(map(object({ + disabled_rules = optional(list(string), []) + exclusions = optional(list(object({ + match_variable = string + operator = string + selector = string + })), []) + })), {}) + })) + default = {} +} + variable "enable_custom_reroute_ruleset" { description = "Toggle on/off the re-routing of traffic accessing the Ruby Complete app under certain request paths which should route to the .NET Complete app backend origin" type = bool diff --git a/waf.tf b/waf.tf index 8f582a0..fa4d1f1 100644 --- a/waf.tf +++ b/waf.tf @@ -21,7 +21,7 @@ resource "azurerm_cdn_frontdoor_firewall_policy" "waf" { content { name = "RateLimiting" enabled = true - priority = 1000 + priority = 2000 rate_limit_duration_in_minutes = local.waf_rate_limiting_duration_in_minutes rate_limit_threshold = local.waf_rate_limiting_threshold type = "RateLimitRule" @@ -48,7 +48,7 @@ resource "azurerm_cdn_frontdoor_firewall_policy" "waf" { } dynamic "managed_rule" { - for_each = local.waf_enable_bot_protection ? [1] : [] + for_each = contains(keys(local.waf_managed_rulesets), "Microsoft_BotManagerRuleSet_1.1") == false && local.waf_enable_bot_protection && azurerm_cdn_frontdoor_profile.rsd[0].sku_name == "Premium_AzureFrontDoor" ? [1] : [] content { type = "Microsoft_BotManagerRuleSet" @@ -57,13 +57,74 @@ resource "azurerm_cdn_frontdoor_firewall_policy" "waf" { } } + dynamic "managed_rule" { + for_each = azurerm_cdn_frontdoor_profile.rsd[0].sku_name == "Premium_AzureFrontDoor" ? local.waf_managed_rulesets : {} + + content { + type = replace(managed_rule.key, "_${element(split("_", managed_rule.key), -1)}", "") + version = element(split("_", managed_rule.key), -1) + action = managed_rule.value.action + + dynamic "override" { + for_each = managed_rule.value.overrides + + content { + rule_group_name = override.key + + dynamic "exclusion" { + for_each = lookup(override.value, "exclusions", []) + + content { + match_variable = exclusion.value.match_variable + operator = exclusion.value.operator + selector = exclusion.value.selector + } + } + + dynamic "rule" { + for_each = toset(lookup(override.value, "disabled_rules", [])) + + content { + rule_id = rule.value + enabled = false + action = "Log" + } + } + } + } + } + } + + dynamic "custom_rule" { + for_each = local.waf_custom_rules + + content { + name = custom_rule.key + enabled = true + priority = custom_rule.value["priority"] + type = "MatchRule" + action = custom_rule.value["action"] + + dynamic "match_condition" { + for_each = custom_rule.value["match_conditions"] + + content { + match_variable = match_condition.value["match_variable"] + match_values = match_condition.value["match_values"] + operator = match_condition.value["operator"] + selector = match_condition.value["selector"] + } + } + } + } + dynamic "custom_rule" { for_each = local.per_origin_custom_rules content { name = replace(custom_rule.key, "/[^[:alnum:]]/", "") enabled = true - priority = index(tolist(keys(local.per_origin_custom_rules)), custom_rule.key) + 1 + priority = (index(tolist(keys(local.per_origin_custom_rules)), custom_rule.key) * 10) + (length(var.waf_custom_rules) > 0 ? sum([for rule in values(var.waf_custom_rules) : rule.priority]) : 10) type = "MatchRule" action = custom_rule.value["action"] @@ -94,7 +155,6 @@ resource "azurerm_cdn_frontdoor_firewall_policy" "waf" { tags = local.tags } - resource "azurerm_cdn_frontdoor_security_policy" "waf" { count = local.enable_frontdoor && length(azurerm_cdn_frontdoor_endpoint.rsd) > 0 ? 1 : 0