A hands-on lab guide for implementing hub-spoke network topology with centralized Azure Firewall for spoke-to-spoke traffic routing.
- Overview
- Architecture
- Prerequisites
- Lab Environment
- Implementation Steps
- Validation
- Cost Management
- Troubleshooting
- Clean Up
This lab demonstrates how to implement a hub-spoke network topology in Azure where all spoke-to-spoke traffic is routed through a centralized Azure Firewall in the hub VNet. This is a common enterprise architecture pattern that provides:
- ✅ Centralized network security and traffic inspection
- ✅ Simplified network management
- ✅ Cost optimization by sharing resources in the hub
- ✅ Network isolation between spokes
+----------------------------------------+
| vnet-spoke-app (10.10.0.0/16) |
|----------------------------------------|
| +----------------------------------+ |
| | snet-app-web (10.10.1.0/24) | |
| +----------------------------------+ |
| |
| +----------------------------------+ |
| | snet-app-backend (10.10.2.0/24) | |
| +----------------------------------+ |
+------------------------▲---------------+
||
|| vNet Peering
||
+----------------------------------------+ || +----------------------------------------+
| vnet-onprem (10.30.0.0/16) | || | vnet-hub (10.0.0.0/16) |
|----------------------------------------| || |----------------------------------------|
| +----------------------------------+ |==========||========| +----------------------------------+ |
| | snet-onprem-lan (10.30.1.0/24) | | vNet Peering | | snet-hub-shared (10.0.1.0/24) | |
| +----------------------------------+ | | +----------------------------------+ |
| | | |
| +----------------------------------+ | | +----------------------------------+ |
| | snet-onprem-lan2 (10.40.1.0/24) | | | | AzureFirewallSubnet (10.0.3.0/26)| |
| +----------------------------------+ | | | [ Azure Firewall ] | |
+----------------------------------------+ | +----------------------------------+ |
+----------------------------------------+
When vm-app (10.10.2.4) pings vm-onprem (10.30.1.4):
vm-app → Route Table → Azure Firewall (10.0.3.4) → Route Table → vm-onprem
│ │ │
└─ Source: 10.10.2.4 └─ Inspects & forwards └─ Destination: 10.30.1.4
- Azure subscription with Contributor access
- Azure CLI installed or access to Azure Cloud Shell
- Basic understanding of:
- Virtual Networks and subnets
- Network Security Groups
- Azure Firewall concepts
| Resource Group | Purpose |
|---|---|
rg-hub-network |
Hub VNet, Firewall, Route Tables |
rg-spoke-app |
Application spoke VNet and VMs |
rg-onprem-sim |
Simulated on-premises VNet and VMs |
| VNet | Address Space | Subnets |
|---|---|---|
| vnet-hub | 10.0.0.0/16 | snet-hub-shared (10.0.1.0/24), AzureFirewallSubnet (10.0.3.0/26) |
| vnet-spoke-app | 10.10.0.0/16 | snet-app-web (10.10.1.0/24), snet-app-backend (10.10.2.0/24) |
| vnet-onprem | 10.30.0.0/16 | snet-onprem-lan (10.30.1.0/24), snet-onprem-lan2 (10.40.1.0/24) |
az group create --name rg-hub-network --location eastus2
az group create --name rg-spoke-app --location eastus2
az group create --name rg-onprem-sim --location eastus2az network vnet create \
--resource-group rg-hub-network \
--name vnet-hub \
--address-prefix 10.0.0.0/16 \
--subnet-name snet-hub-shared \
--subnet-prefix 10.0.1.0/24
⚠️ Important: The subnet name must be exactlyAzureFirewallSubnetand minimum size /26
az network vnet subnet create \
--resource-group rg-hub-network \
--vnet-name vnet-hub \
--name AzureFirewallSubnet \
--address-prefix 10.0.3.0/26# Spoke App VNet
az network vnet create \
--resource-group rg-spoke-app \
--name vnet-spoke-app \
--address-prefix 10.10.0.0/16 \
--subnet-name snet-app-web \
--subnet-prefix 10.10.1.0/24
az network vnet subnet create \
--resource-group rg-spoke-app \
--vnet-name vnet-spoke-app \
--name snet-app-backend \
--address-prefix 10.10.2.0/24
# OnPrem Simulation VNet
az network vnet create \
--resource-group rg-onprem-sim \
--name vnet-onprem \
--address-prefix 10.30.0.0/16 \
--subnet-name snet-onprem-lan \
--subnet-prefix 10.30.1.0/24# Hub to Spoke-App
az network vnet peering create \
--resource-group rg-hub-network \
--name hub-to-spoke-app \
--vnet-name vnet-hub \
--remote-vnet /subscriptions/<subscription-id>/resourceGroups/rg-spoke-app/providers/Microsoft.Network/virtualNetworks/vnet-spoke-app \
--allow-forwarded-traffic \
--allow-vnet-access
# Spoke-App to Hub
az network vnet peering create \
--resource-group rg-spoke-app \
--name spoke-app-to-hub \
--vnet-name vnet-spoke-app \
--remote-vnet /subscriptions/<subscription-id>/resourceGroups/rg-hub-network/providers/Microsoft.Network/virtualNetworks/vnet-hub \
--allow-forwarded-traffic \
--allow-vnet-access# Hub to OnPrem
az network vnet peering create \
--resource-group rg-hub-network \
--name hub-to-onprem \
--vnet-name vnet-hub \
--remote-vnet /subscriptions/<subscription-id>/resourceGroups/rg-onprem-sim/providers/Microsoft.Network/virtualNetworks/vnet-onprem \
--allow-forwarded-traffic \
--allow-vnet-access
# OnPrem to Hub
az network vnet peering create \
--resource-group rg-onprem-sim \
--name onprem-to-hub \
--vnet-name vnet-onprem \
--remote-vnet /subscriptions/<subscription-id>/resourceGroups/rg-hub-network/providers/Microsoft.Network/virtualNetworks/vnet-hub \
--allow-forwarded-traffic \
--allow-vnet-accessEnsure both peerings have these settings enabled:
| Setting | Value |
|---|---|
| Allow virtual network access | ✅ Enabled |
| Allow forwarded traffic | ✅ Enabled |
| Peering state | Connected |
⚠️ Critical: You must enable "Allow forwarded traffic" on both sides of each peering connection. Without this setting, traffic routed through the Azure Firewall will be dropped, and spoke-to-spoke communication will fail. This is the most common misconfiguration in hub-spoke architectures.
az network firewall policy create \
--resource-group rg-hub-network \
--name fw-policy-hub \
--location eastus2az network public-ip create \
--resource-group rg-hub-network \
--name fw-hub-pip \
--sku Standard \
--allocation-method Static
⚠️ Cost Warning: Azure Firewall Standard costs$1.25/hour ($900/month). Useaz network firewall deallocatewhen not in use.
az network firewall create \
--resource-group rg-hub-network \
--name fw-hub \
--location eastus2 \
--vnet-name vnet-hub \
--firewall-policy fw-policy-hub \
--public-ip fw-hub-pip \
--sku AZFW_VNet \
--tier Standardaz network firewall show \
--resource-group rg-hub-network \
--name fw-hub \
--query "ipConfigurations[0].privateIPAddress" \
--output tsv📝 Note: Save this IP (e.g.,
10.0.3.4). You'll need it for route tables.
az network route-table create \
--resource-group rg-hub-network \
--name rt-spoke-to-spoke \
--location eastus2Route traffic from Spoke-App to OnPrem through Firewall:
az network route-table route create \
--resource-group rg-hub-network \
--route-table-name rt-spoke-to-spoke \
--name to-onprem \
--address-prefix 10.30.1.0/24 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.3.4Route traffic from OnPrem to Spoke-App through Firewall:
az network route-table route create \
--resource-group rg-hub-network \
--route-table-name rt-spoke-to-spoke \
--name to-spoke-app-web \
--address-prefix 10.10.1.0/24 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.3.4
az network route-table route create \
--resource-group rg-hub-network \
--route-table-name rt-spoke-to-spoke \
--name to-spoke-app-backend \
--address-prefix 10.10.2.0/24 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.3.4# Associate to Spoke-App subnets
az network vnet subnet update \
--resource-group rg-spoke-app \
--vnet-name vnet-spoke-app \
--name snet-app-web \
--route-table /subscriptions/<subscription-id>/resourceGroups/rg-hub-network/providers/Microsoft.Network/routeTables/rt-spoke-to-spoke
az network vnet subnet update \
--resource-group rg-spoke-app \
--vnet-name vnet-spoke-app \
--name snet-app-backend \
--route-table /subscriptions/<subscription-id>/resourceGroups/rg-hub-network/providers/Microsoft.Network/routeTables/rt-spoke-to-spoke
# Associate to OnPrem subnet
az network vnet subnet update \
--resource-group rg-onprem-sim \
--vnet-name vnet-onprem \
--name snet-onprem-lan \
--route-table /subscriptions/<subscription-id>/resourceGroups/rg-hub-network/providers/Microsoft.Network/routeTables/rt-spoke-to-spoke
⚠️ Critical: Route tables must be associated with the correct subnets in each spoke VNet. Creating routes alone is not enough—you must go to the Route Table → Subnets → Associate and attach it to each subnet that needs to route traffic through the firewall. If the association is missing, traffic will use default Azure routing and bypass the firewall.
| Route Name | Address Prefix | Next Hop Type | Next Hop IP |
|---|---|---|---|
| to-onprem | 10.30.1.0/24 | VirtualAppliance | 10.0.3.4 |
| to-spoke-app-web | 10.10.1.0/24 | VirtualAppliance | 10.0.3.4 |
| to-spoke-app-backend | 10.10.2.0/24 | VirtualAppliance | 10.0.3.4 |
Portal Navigation: Firewall Policy → Rule Collections → Add a rule collection
| Setting | Value |
|---|---|
| Rule collection group | DefaultNetworkRuleCollectionGroup |
| Rule collection name | allow-spoke-traffic |
| Rule collection type | Network |
| Priority | 100 |
| Action | Allow |
| Rule | Source | Destination | Protocol | Port |
|---|---|---|---|---|
| allow-spoke-to-spoke | 10.10.1.0/24,10.10.2.0/24,10.30.1.0/24 | 10.10.1.0/24,10.10.2.0/24,10.30.1.0/24 | Any | * |
az vm create \
--resource-group rg-spoke-app \
--name vm-app \
--image Ubuntu2204 \
--vnet-name vnet-spoke-app \
--subnet snet-app-backend \
--admin-username azureuser \
--generate-ssh-keys \
--size Standard_B1saz vm create \
--resource-group rg-onprem-sim \
--name vm-onprem \
--image Ubuntu2204 \
--vnet-name vnet-onprem \
--subnet snet-onprem-lan \
--admin-username azureuser \
--generate-ssh-keys \
--size Standard_B1sFrom vm-app, ping vm-onprem:
ping 10.30.1.4From vm-onprem, ping vm-app:
ping 10.10.2.4PING 10.30.1.4 (10.30.1.4) 56(84) bytes of data.
64 bytes from 10.30.1.4: icmp_seq=1 ttl=63 time=2.64 ms
64 bytes from 10.30.1.4: icmp_seq=2 ttl=63 time=2.56 ms
64 bytes from 10.30.1.4: icmp_seq=3 ttl=63 time=3.05 ms
📝 Note: TTL=63 indicates traffic passed through the firewall (TTL decremented by 1)
az network watcher show-next-hop \
--resource-group rg-spoke-app \
--vm vm-app \
--source-ip 10.10.2.4 \
--dest-ip 10.30.1.4Expected output:
{
"nextHopIpAddress": "10.0.3.4",
"nextHopType": "VirtualAppliance",
"routeTableId": "..."
}| Resource | Running | Deallocated |
|---|---|---|
| Azure Firewall Standard | ~$1.25/hour | $0 |
| VM (Standard_B1s) | ~$0.01/hour | $0 |
| Managed Disk | ~$1-5/month | ~$1-5/month |
| Public IP (Static) | ~$3-4/month | ~$3-4/month |
| VNet, Peering, Route Table | $0 | $0 |
# Deallocate Firewall
az network firewall deallocate \
--resource-group rg-hub-network \
--name fw-hub
# Deallocate VMs
az vm deallocate --resource-group rg-spoke-app --name vm-app --no-wait
az vm deallocate --resource-group rg-onprem-sim --name vm-onprem --no-waitaz network firewall update \
--resource-group rg-hub-network \
--name fw-hub \
--vnet-name vnet-hub \
--public-ip fw-hub-pipaz network vnet peering list \
--resource-group rg-hub-network \
--vnet-name vnet-hub \
--query "[].{name:name, state:peeringState, allowForwardedTraffic:allowForwardedTraffic}" \
-o table✅ Expected: State = Connected, allowForwardedTraffic = true
az network vnet subnet show \
--resource-group rg-spoke-app \
--vnet-name vnet-spoke-app \
--name snet-app-backend \
--query "routeTable.id" \
-o tsv✅ Expected: Returns route table ID (not empty)
az network watcher show-next-hop \
--resource-group rg-spoke-app \
--vm vm-app \
--source-ip 10.10.2.4 \
--dest-ip 10.30.1.4✅ Expected: nextHopType = VirtualAppliance, nextHopIpAddress = 10.0.3.4
❌ If nextHopType = None: Route table not associated or missing route
az network firewall show \
--resource-group rg-hub-network \
--name fw-hub \
--query "provisioningState" \
-o tsv✅ Expected: Succeeded
az network firewall policy rule-collection-group list \
--policy-name fw-policy-hub \
--resource-group rg-hub-network \
-o json✅ Expected: Network rule with source/destination matching spoke subnets, protocol = Any
Cause: Existing subnet uses an IP range outside the VNet address space
Solution:
- Expand VNet address space to cover all subnets
- Or remove conflicting subnets and recreate
Cause: Active VNet peering may block changes (older behavior)
Solution:
- Try adding address space and then sync peering
- Or temporarily delete peering → add address space → recreate peering
Common Causes:
- Subnet name must be exactly
AzureFirewallSubnet - Minimum size is /26
- Cannot overlap with existing subnets
Cause: Missing return route
Solution: Ensure both directions have routes:
- Spoke → OnPrem: Route in spoke subnet pointing to FW
- OnPrem → Spoke: Route in onprem subnet pointing to FW
az group delete --name rg-hub-network --yes --no-wait
az group delete --name rg-spoke-app --yes --no-wait
az group delete --name rg-onprem-sim --yes --no-wait- Azure Hub-Spoke Architecture
- Azure Firewall Documentation
- Virtual Network Peering
- User-Defined Routes
This lab guide is provided for educational purposes.