Using Azure Firewall as a Network Virtual Appliance (NVA) provides a powerful alternative to traditional VNet peering in hub-and-spoke designs. Instead of relying on implicit trust between spokes, all inter-VNet and outbound traffic can be routed through a central inspection point where rules, logging, and governance are enforced.
This post builds on the FastTrack for Azure article “Using Azure Firewall as a network virtual appliance (NVA)” and reproduces a similar architecture using Terraform. The goal is to demonstrate how the entire setup can be deployed as Infrastructure-as-Code, including optional integration with Azure Network Manager.
– Big thanks to DJBartles for that great blog article! Feel free to check his blog for more information.
Contents
Why Use Azure Firewall as an NVA Instead of Native Peering?
Native VNet peering creates a high-trust path between networks. This is simple and fast, but it offers little visibility or control. By placing a firewall in the hub and routing traffic through it, you gain:
- Central policy enforcement across all spokes
- Full logging and visibility via Azure Monitor
- Consistent traffic inspection for both east-west and outbound flows
- Better governance for enterprise networks
- Scalability when combined with Azure Network Manager
This design is especially helpful when more spokes or teams join the environment over time.
Terraform Deployment Overview
The Terraform code for this blog post is divided into several logical sections. Instead of exposing one large file, we walk through the important parts step by step — firewall, policies, VNets, routing, and the optional Network Manager approach.
See the full code on Github
1. Basic Components: Firewall, Policy, and Rules
We start with the essentials needed to run Azure Firewall:
Firewall, Firewall Policy & Rule Collection (Snippet)
# ============================================================================
# Firewall
# ============================================================================
# Public IP für Firewall
resource "azurerm_public_ip" "firewall" {
name = "pip-firewall"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
# Firewall Policy
resource "azurerm_firewall_policy" "policy" {
name = "fw-policy"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku = "Standard"
}
resource "azurerm_firewall_policy_rule_collection_group" "network_rules" {
name = "network-rules"
firewall_policy_id = azurerm_firewall_policy.policy.id
priority = 100
network_rule_collection {
name = "allow-spoke-to-spoke"
priority = 100
action = "Allow"
###
#Required Rules - these can be seen as the peering between the spoke vnets
###
rule {
name = "spoke1-to-spoke2"
protocols = ["Any"]
source_addresses = ["10.1.0.0/16"]
destination_addresses = ["10.2.0.0/16"]
destination_ports = ["*"]
}
rule {
name = "spoke2-to-spoke1"
protocols = ["Any"]
source_addresses = ["10.2.0.0/16"]
destination_addresses = ["10.1.0.0/16"]
destination_ports = ["*"]
}
}
}
# Azure Firewall
resource "azurerm_firewall" "fw" {
name = "fw-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku_name = "AZFW_VNet"
sku_tier = "Standard"
firewall_policy_id = azurerm_firewall_policy.policy.id
ip_configuration {
name = "configuration"
subnet_id = azurerm_subnet.firewall.id
public_ip_address_id = azurerm_public_ip.firewall.id
}
}2. VNets and Hub–Spoke Peerings
We deploy one hub VNet and two spokes.
Each spoke contains a workload subnet, and both spokes are peered to the hub.
VNet & Peering Snippets
# ============================================================================
# Vnets & Subnets
# ============================================================================
# Hub VNet
resource "azurerm_virtual_network" "hub" {
name = "vnet-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "firewall" {
name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = ["10.0.1.0/26"]
}
# Spoke 1 VNet
resource "azurerm_virtual_network" "spoke1" {
name = "vnet-spoke1"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = ["10.1.0.0/16"]
}
resource "azurerm_subnet" "spoke1_workload" {
name = "snet-workload"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.spoke1.name
address_prefixes = ["10.1.1.0/24"]
}
# Spoke 2 VNet
resource "azurerm_virtual_network" "spoke2" {
name = "vnet-spoke2"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = ["10.2.0.0/16"]
}
resource "azurerm_subnet" "spoke2_workload" {
name = "snet-workload"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.spoke2.name
address_prefixes = ["10.2.1.0/24"]
}
# ============================================================================
# Peerings to Hub
# ============================================================================
#Peerings
resource "azurerm_virtual_network_peering" "hub_to_spoke1" {
name = "peer-hub-to-spoke1"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.hub.name
remote_virtual_network_id = azurerm_virtual_network.spoke1.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
use_remote_gateways = false
}
resource "azurerm_virtual_network_peering" "spoke1_to_hub" {
name = "peer-spoke1-to-hub"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.spoke1.name
remote_virtual_network_id = azurerm_virtual_network.hub.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
use_remote_gateways = false
}
resource "azurerm_virtual_network_peering" "hub_to_spoke2" {
name = "peer-hub-to-spoke2"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.hub.name
remote_virtual_network_id = azurerm_virtual_network.spoke2.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
use_remote_gateways = false
}
resource "azurerm_virtual_network_peering" "spoke2_to_hub" {
name = "peer-spoke2-to-hub"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.spoke2.name
remote_virtual_network_id = azurerm_virtual_network.hub.id
allow_virtual_network_access = true
allow_forwarded_traffic = true
use_remote_gateways = false
}Important: allowForwardedTraffic = true is required so the spokes accept traffic being forwarded through the firewall.
3. Routing Through the Firewall
To use Azure Firewall as an NVA, traffic must be redirected through its private IP.
This post shows two variants:
Variant 1: Direct UDR Assignment
This is the simplest approach — a default route points to the firewall.
UDR Snippet & Associations to the spoke subnets
# ============================================================================
# Deploy UDR directly
# ============================================================================
resource "azurerm_route_table" "udr_to_fw" {
name = "rt-route-to-fw"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
route = [
{
name = "fwroute"
address_prefix = "0.0.0.0/0"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = azurerm_firewall.fw.ip_configuration[0].private_ip_address
}
]
}
resource "azurerm_subnet_route_table_association" "spoke1_rt" {
route_table_id = azurerm_route_table.udr_to_fw.id
subnet_id = azurerm_subnet.spoke1_workload.id
}
resource "azurerm_subnet_route_table_association" "spoke2_rt" {
route_table_id = azurerm_route_table.udr_to_fw.id
subnet_id = azurerm_subnet.spoke2_workload.id
}This method is easy to manage in small environments but becomes harder to scale when new spokes appear.
Variant 2: Routing via Azure Network Manager
Azure Network Manager (ANM) brings centralized, policy-driven routing.
It is highly recommended in larger or frequently changing environments.
Why Azure Network Manager?
- Centralized routing governance
- Automatic configuration rollout
- Dynamic grouping of VNets
- Cleaner separation of responsibilities (Connectivity, Security, Routing)
Network Manager & Routing Snippet
# ============================================================================
# Deploy UDR via Network Manager
# ============================================================================
# Network Manager
resource "azurerm_network_manager" "nm" {
name = "nm-hub-spoke"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
scope {
subscription_ids = [data.azurerm_subscription.current.id]
}
scope_accesses = ["Connectivity", "SecurityAdmin", "Routing"]
}
# Network Group für Spokes
resource "azurerm_network_manager_network_group" "spokes" {
name = "ng-spokes"
network_manager_id = azurerm_network_manager.nm.id
}
# Routing Configuration - User Defined Routes zu Firewall
resource "azurerm_network_manager_routing_configuration" "routing" {
name = "routing-to-firewall"
network_manager_id = azurerm_network_manager.nm.id
}
resource "azurerm_network_manager_routing_rule_collection" "spoke_routes" {
name = "spoke-to-firewall-routes"
routing_configuration_id = azurerm_network_manager_routing_configuration.routing.id
network_group_ids = [azurerm_network_manager_network_group.spokes.id]
}
resource "azurerm_network_manager_routing_rule" "routing_rule" {
name = "default_to_fwnva"
rule_collection_id = azurerm_network_manager_routing_rule_collection.spoke_routes.id
description = "Default Route to Azure Firewall"
destination {
type = "AddressPrefix"
address = "0.0.0.0/0"
}
next_hop {
address = "10.0.1.4" #azurerm_firewall.fw.ip_configuration[0].private_ip_address
type = "VirtualAppliance"
}
}
# Deployment der Routing Configuration
resource "azurerm_network_manager_deployment" "routing_deployment" {
network_manager_id = azurerm_network_manager.nm.id
location = azurerm_resource_group.rg.location
scope_access = "Routing"
configuration_ids = [azurerm_network_manager_routing_configuration.routing.id]
depends_on = [azurerm_network_manager.nm, azurerm_network_manager_routing_rule.routing_rule]
}How to add Vnets to the Network Group?
Static Network Group Membership – manually
resource "azurerm_network_manager_static_member" "spoke1" {
name = "spoke1-member"
network_group_id = azurerm_network_manager_network_group.spokes.id
target_virtual_network_id = azurerm_virtual_network.spoke1.id
}
resource "azurerm_network_manager_static_member" "spoke2" {
name = "spoke2-member"
network_group_id = azurerm_network_manager_network_group.spokes.id
target_virtual_network_id = azurerm_virtual_network.spoke2.id
}Good for controlled environments, but requires manual updates.
Dynamic Membership via Azure Policy
resource "azurerm_policy_definition" "vnet_to_network_group" {
name = "add-vnet-to-network-group"
policy_type = "Custom"
mode = "Microsoft.Network.Data"
display_name = "Add VNets with specific name to Network Group"
metadata = jsonencode({
category = "Network"
})
policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.Network/virtualNetworks"
},
{
field = "name"
contains = "spoke"
}
]
}
then = {
effect = "addToNetworkGroup"
details = {
networkGroupId = azurerm_network_manager_network_group.spokes.id
}
}
})
}
resource "azurerm_subscription_policy_assignment" "nwm-group-assignment" {
name = "assign-vnet-policy"
policy_definition_id = azurerm_policy_definition.vnet_to_network_group.id
subscription_id = data.azurerm_subscription.current.id
}Why policies for membership?
- Zero operational effort
- Perfect for large or automated deployments
- Ensures consistent routing behavior across environments
- Spokes are added automatically if their name contains “spoke” – or how you want
Summary
In this post, we deployed a complete hub-and-spoke network with Azure Firewall acting as a central NVA. You saw how to:
- Build the hub, spokes, firewall, and routing configuration
- Route traffic through Azure Firewall via UDR or Network Manager
- Use Azure Policy to automate network group membership
The Network Manager approach is especially useful for enterprises that need scalable, centrally governed routing.
Which approach do you use? Direct peering or a centralized Firewall for more control?