Firewall rules should protect your network, not your free time. Yet too often, updating them is a tedious, error-prone slog. In this series, I’ll show how turning firewall rules into code can flip that script.


I’d recently been working with a customer on a project to migrate to a new hub network, and one of the challenges we faced was how best to manage Azure Firewall rules. I wanted a way to implement firewall rules as code to bring consistency and automation, but also ensure that users could update the firewall without needing deep infrastructure‑as‑code skills in tools like Bicep.

While IaC frameworks deliver immense value in terms of repeatability and governance, they can unintentionally create a barrier for day‑to‑day operations. Not every team member is fluent in Bicep or ARM, and requiring deep IaC knowledge for something as routine as updating a firewall rule often slows down collaboration. This gap leads to two common outcomes: either infrastructure specialists become bottlenecks for simple changes, or users bypass automation altogether by making ad‑hoc edits in the Azure portal introducing drift, inconsistency, and compliance risks. What’s needed is a middle ground: a way to preserve the rigor of IaC while giving non‑specialists a safe, accessible way to contribute.

While exploring options, I came across the excellent GitHub project AzureFirewallRulesAsCode, which provides a practical approach to defining and managing firewall rules in a way that’s both accessible and scalable. To tailor it to the needs of my customer project, I forked the repository and began experimenting with how the CSV‑based workflow could fit into our environment. This allowed me to adapt the scripts and pipelines to our specific governance requirements while still benefiting from the simplicity and automation the project offers. By forking it, I could also contribute back improvements and share lessons learned with the wider community.

As I worked with the AzureFirewallRulesAsCode project, I saw an opportunity to extend its value by adding a lab deployment option. The idea was to let users spin up a conceptual hub network in Azure, giving them a safe space to experiment with firewall rule automation before committing to production. For our customer project, we were designing a new hub network architecture, so I utilised and integrated the AVM hub networking module to deploy a complete hub setup. This includes core components like firewalls, route tables and virtual networks, just like the ones shown in the image below. To make the lab even more flexible, I added an optional switch to include private DNS zones, enabling realistic private link scenarios. Beyond the hub module, the deployment also leverages supporting AVM modules for network security groups, resource groups, firewall policy, and other foundational services—ensuring the lab is modular, reusable, and production-aligned.

If you’re keen to try it out, be sure to check the 🚀 Quick Start guide in the repo’s README — it walks you through everything you need to get going.

Once the hub networking was in place, it was time to start experimenting with the script. Since the hub was being deployed using Bicep, I wanted to centralise our configuration and avoid scattering parameters across files. The obvious choice was to extract them directly from the .bicepparam file. Using the bicep build-params command, I was able to pull out the parameters cleanly and convert them into a usable object. You’ll see that in the script here:

$parameters = (bicep build-params $TemplateParameterFile --stdout | ConvertFrom-Json).parametersJson | ConvertFrom-Json

Whilst these can still be provided manually, I like to reduce the input on my deployments as much as possible

Next, I turned my attention to how IP groups were handled. Since we’d already extracted key variables like the subscription ID and resource group name from the previous step, it made sense to put them to use. I wanted the script to dynamically build the full resource ID for each IP group, keeping things flexible and environment-aware. That led to this simple line:

$ipGroupId = "/subscriptions/$SubscriptionId/resourceGroups/$ipGroupRg/providers/Microsoft.Network/ipGroups/$ipGroupName"

The next change came in the form of the Bicep template itself. I wanted to target the child resource — Microsoft.Network/firewallPolicies/ruleCollectionGroups — rather than the top-level firewall policy. This shift required a few updates to the script, but the main driver was TLS inspection. The customer needed TLS inspection enabled, and deploying the full Firewall Policy module would overwrite that setting. By focusing on the Rule Collection Group instead, we ensured that TLS inspection remained intact and could be controlled at the rule level, giving us the flexibility we needed without compromising the configuration.

Below shows the module:


// =============================
// Azure Firewall Policy Rule Collection Group Module
// =============================
//
// This Bicep file defines the deployment of a Firewall Policy Rule Collection Group for Azure Firewall.
// It is designed to be used as part of a modular hub networking solution, or standalone.
//
// Key sections:
//   - Parameters for parent policy, group name, priority, and rule collections
//   - Existing resource reference for parent Firewall Policy
//   - Resource deployment for Rule Collection Group
//
// Parameters:
//   - firewallPolicyName: (string) Conditional. The name of the parent Firewall Policy. Required if used standalone.
//   - name: (string) Required. The name of the rule collection group to deploy.
//   - priority: (int) Required. Priority of the Firewall Policy Rule Collection Group resource.
//   - ruleCollections: (array?) Optional. Group of Firewall Policy rule collections.
//
// Usage Example:
//   module fwPolicyRuleCollectionGroup 'fwpolicyrulecollectiongroup.bicep' = {
//     name: 'myRuleCollectionGroup'
//     params: {
//       firewallPolicyName: 'myFirewallPolicy'
//       name: 'myRuleCollectionGroup'
//       priority: 100
//       ruleCollections: [ ... ]
//     }
//   }
// =============================


metadata name = 'Firewall Policy Rule Collection Groups'
metadata description = 'This module deploys a Firewall Policy Rule Collection Group.'

@description('Conditional. The name of the parent Firewall Policy. Required if the template is used in a standalone deployment.')
param firewallPolicyName string

@description('Required. The name of the rule collection group to deploy.')
param name string

@description('Required. Priority of the Firewall Policy Rule Collection Group resource.')
param priority int

@description('Optional. Group of Firewall Policy rule collections.')
param ruleCollections array?

resource firewallPolicy 'Microsoft.Network/firewallPolicies@2023-04-01' existing = {
  name: firewallPolicyName
}

// Firewall Policy Module

resource resFirewallPolicy 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2024-07-01' =  {
  name: name
  parent: firewallPolicy
  properties: {
    priority: priority
    ruleCollections: ruleCollections ?? []
  }
}

With the hub networking deployed and environment variables imported, I moved on to manually testing the script. This was the moment to validate that everything, from parameter extraction to rule deployment, worked as expected. I used an example CSV file to drive the test, which you can find here.

This particular example doesn’t use IP groups, but if you want to incorporate them, it’s straightforward. Just set the sourceType or destinationType to sourceIpGroups or destinationIpGroups, and use the name of the IP group in the corresponding field. The script will dynamically build the full resource ID and inject it into the firewall policy rule collection — no hardcoding required.

With the provided example, I had a successful deployment of the firewall rules as shown below.

This concludes the first part of a three-part series. In the next installment, I’ll walk through integrating this workflow with Azure DevOps to bring CI/CD into the picture. Finally, in part three, we’ll look at further improvements — including test scripts to validate formatting and adding basic guardrails to catch risky configurations like any-any rules before they slip through.

Tried the lab? Got feedback or ideas? Drop a comment or connect on LinkedIn

I’d love to hear how you’re tackling firewall automation.


Leave a Reply

Your email address will not be published. Required fields are marked *