Post

Dynamically managing rules in Azure Firewall in Terraform

Intro

The purpose of this post is to give you inspiration, on how to create a dynamic Terraform configuration for Azure Firewall rules.

Let me know if you have suggestions for improvements.

Disclaimer

The code has been used as PoC in my lab. It’s not meant for production.

Code

The code shown below is just the snippets of the configuration.

You can see the full code at the link down below: https://github.com/danielthorvil/danielthorvil.github.io/tree/main/assets/post-content/0003/tf/

main.tf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# Azure Firewall policy
resource "azurerm_firewall_policy" "azfw_policy" {
  name                     = "${var.name-prefix}-azfw-policy"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = var.region
  sku                      = "Basic"
  threat_intelligence_mode = "Alert"
}

# IP Groups
resource "azurerm_ip_group" "ip_groups" {
  resource_group_name = azurerm_resource_group.rg.name
  location            = var.region

  for_each = { for ipgroup in var.ip-groups : ipgroup.name => ipgroup.cidrs }

  name  = each.key
  cidrs = each.value
}

locals {
  # Used for lookup in the azurerm_firewall_policy_rule_collection_group.azfw_rules resource.
  ip_group_mapping = { for ip_group in azurerm_ip_group.ip_groups : ip_group.name => ip_group.id }
}

# Rule collection groups, rule collections, rules
resource "azurerm_firewall_policy_rule_collection_group" "azfw_rules" {
  firewall_policy_id = azurerm_firewall_policy.azfw_policy.id

  for_each = { for ruleCollectionGroup in var.ruleCollectionGroups : ruleCollectionGroup.ruleCollectionGroupName => ruleCollectionGroup }

  name     = each.key
  priority = each.value.ruleCollectionGroupPriority

  dynamic "network_rule_collection" {
    for_each = { for ruleCollection in coalesce(each.value.NetworkRuleCollections, []) : ruleCollection.ruleCollectionName => ruleCollection }

    content {
      name     = network_rule_collection.value.ruleCollectionName
      priority = network_rule_collection.value.ruleCollectionPriority
      action   = network_rule_collection.value.ruleCollectionAction

      dynamic "rule" {
        for_each = { for rule in network_rule_collection.value.rules : rule.name => rule }

        content {
          name                  = rule.value.name
          description           = rule.value.description
          source_addresses      = rule.value.source_addresses
          source_ip_groups      = [for ip_group in coalesce(rule.value.source_ip_groups, []) : lookup(local.ip_group_mapping, ip_group)]
          destination_addresses = rule.value.destination_addresses
          destination_ip_groups = [for ip_group in coalesce(rule.value.destination_ip_groups, []) : lookup(local.ip_group_mapping, ip_group)]
          destination_fqdns     = rule.value.destination_fqdns
          protocols             = rule.value.protocols
          destination_ports     = rule.value.destination_ports
        }
      }
    }
  }

  dynamic "application_rule_collection" {
    for_each = { for ruleCollection in coalesce(each.value.ApplicationRuleCollections, []) : ruleCollection.ruleCollectionName => ruleCollection if ruleCollection.ruleCollectionName != null }

    content {
      name     = application_rule_collection.value.ruleCollectionName
      priority = application_rule_collection.value.ruleCollectionPriority
      action   = application_rule_collection.value.ruleCollectionAction

      dynamic "rule" {
        for_each = { for rule in application_rule_collection.value.rules : rule.name => rule }

        content {
          name              = rule.value.name
          description       = rule.value.description
          source_addresses  = rule.value.source_addresses
          source_ip_groups  = [for ip_group in coalesce(rule.value.source_ip_groups, []) : lookup(local.ip_group_mapping, ip_group)]
          destination_fqdns = rule.value.destination_fqdns
          dynamic "protocols" {
            for_each = { for protocol in rule.value.protocols : "${protocol.type}+${protocol.port}" => protocol }

            content {
              type = protocols.value.type
              port = protocols.value.port
            }
          }
        }
      }
    }
  }
}

terraform.tfvars

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
ip-groups = [
  {
    name  = "cloudflareDnsResolvers"
    cidrs = ["1.1.1.0/29", "1.0.0.0/29"]
  },
  {
    name  = "googleDnsResolvers"
    cidrs = ["8.8.8.8/32", "8.8.4.4/32"]
  },
  {
    name  = "localNetworkSpoke01Subnet01"
    cidrs = ["10.69.10.0/24"]
}]

ruleCollectionGroups = [{
  ruleCollectionGroupName     = "ruleCollectionGroup01"
  ruleCollectionGroupPriority = 120

  NetworkRuleCollections = [{
    ruleCollectionName     = "networkRuleCollection01"
    ruleCollectionPriority = 150
    ruleCollectionAction   = "Allow"
    rules = [{
      name                  = "rule01"
      description           = "description"
      source_addresses      = ["10.69.69.0/24"]
      destination_addresses = ["66.66.66.0/32"]
      protocols             = ["ICMP"]
      destination_ports     = ["*"]
      },
      {
        name                  = "rule02"
        description           = "description"
        source_addresses      = ["10.69.69.0/24"]
        destination_addresses = ["65.34.23.4/32"]
        protocols             = ["TCP"]
        destination_ports     = ["443"]
    }]
  }]

  ApplicationRuleCollections = [{
    ruleCollectionName     = "applicationRuleCollection01"
    ruleCollectionPriority = 160
    ruleCollectionAction   = "Allow"
    rules = [{
      name              = "rule01"
      description       = "description"
      source_addresses  = ["10.66.66.0/24"]
      destination_fqdns = ["google.com"]
      protocols         = [{ type = "Https", port = 443 }]
      },
      {
        name              = "rule02"
        description       = "description"
        source_addresses  = ["10.66.66.0/24"]
        destination_fqdns = ["facebook.com"]
        protocols         = [{ type = "Http", port = 80 }]
    }]
  }]
  },
  {
    ruleCollectionGroupName     = "ruleCollectionGroup02"
    ruleCollectionGroupPriority = 120

    NetworkRuleCollections = [{
      ruleCollectionName     = "networkRuleCollection02"
      ruleCollectionPriority = 150
      ruleCollectionAction   = "Allow"
      rules = [{
        name                  = "rule01"
        description           = "description"
        source_addresses      = ["10.69.69.0/24"]
        destination_ip_groups = ["cloudflareDnsResolvers", "googleDnsResolvers"]
        protocols             = ["TCP", "UDP"]
        destination_ports     = ["53"]
      }]
    }]

    ApplicationRuleCollections = [{
      ruleCollectionName     = "applicationRuleCollection02"
      ruleCollectionPriority = 160
      ruleCollectionAction   = "Allow"
      rules = [
        {
          name              = "rule01"
          description       = "description"
          source_ip_groups  = ["localNetworkSpoke01Subnet01"]
          destination_fqdns = ["youtube.com"]
          protocols         = [{ type = "Http", port = 80 }]
      }]
    }]
}]

variables.tf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
variable "ip-groups" {
  description = "Contains the Azure IP Groups that should be created."

  type = list(object({
    name  = string
    cidrs = list(string)
  }))
}

variable "ruleCollectionGroups" {
  description = "Variable that contains rule collections AND the rules within"

  type = list(object({
    ruleCollectionGroupName     = string
    ruleCollectionGroupPriority = number

    NetworkRuleCollections = optional(list(object({
      ruleCollectionName     = string
      ruleCollectionPriority = number
      ruleCollectionAction   = string
      rules = list(object({
        name                  = string
        description           = string
        source_addresses      = optional(list(string))
        source_ip_groups      = optional(list(any))
        destination_addresses = optional(list(string))
        destination_ip_groups = optional(list(any))
        destination_fqdns     = optional(list(string))
        protocols             = list(string)
        destination_ports     = list(any)
    })) })))

    ApplicationRuleCollections = optional(list(object({
      ruleCollectionName     = string
      ruleCollectionPriority = number
      ruleCollectionAction   = string
      rules = list(object({
        name              = string
        description       = string
        source_addresses  = optional(list(string))
        source_ip_groups  = optional(list(any))
        destination_fqdns = list(string)
        protocols = list(object({
          port = any
          type = string
        }))
    })) })))
  }))
}
This post is licensed under CC BY 4.0 by the author.