Skip to content

Project 1: Hub-Spoke Network Architecture

Overview

Build an enterprise-grade hub-spoke network topology with centralized security, routing, and connectivity. This is the most common enterprise network pattern in Azure.

Difficulty: Intermediate to Advanced
Duration: 4-6 hours
Cost: ~$150-200/month if running 24/7 (Azure Firewall is the main cost)

Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              INTERNET                                        │
└─────────────────────────────────────────────────────────────────────────────┘

                                    │ Public IP

┌─────────────────────────────────────────────────────────────────────────────┐
│                           HUB VNET (10.0.0.0/16)                            │
│  ┌─────────────────┐  ┌──────────────────┐  ┌─────────────────────────────┐ │
│  │  Azure Firewall │  │   VPN Gateway    │  │       Azure Bastion         │ │
│  │   (10.0.1.0/26) │  │   (10.0.2.0/27)  │  │       (10.0.3.0/27)         │ │
│  │                 │  │                  │  │                             │ │
│  │  ┌───────────┐  │  │  S2S VPN to      │  │   Secure RDP/SSH access     │ │
│  │  │ FW Policy │  │  │  On-premises     │  │   to all VMs                │ │
│  │  └───────────┘  │  │                  │  │                             │ │
│  └────────┬────────┘  └──────────────────┘  └─────────────────────────────┘ │
│           │                                                                  │
│  ┌────────┴────────┐                                                        │
│  │  Route Table    │  Routes all traffic through Azure Firewall             │
│  │  (UDR)          │                                                        │
│  └────────┬────────┘                                                        │
└───────────┼─────────────────────────────────────────────────────────────────┘

   ┌────────┴────────┬─────────────────────────┐
   │ VNet Peering    │ VNet Peering            │ VNet Peering
   ▼                 ▼                         ▼
┌───────────────┐  ┌───────────────┐  ┌─────────────────────┐
│  SPOKE 1      │  │  SPOKE 2      │  │  SPOKE 3            │
│  WEB TIER     │  │  APP TIER     │  │  DATA TIER          │
│ (10.1.0.0/16) │  │ (10.2.0.0/16) │  │  (10.3.0.0/16)      │
│               │  │               │  │                     │
│ ┌───────────┐ │  │ ┌───────────┐ │  │ ┌─────────────────┐ │
│ │  Web VMs  │ │  │ │  App VMs  │ │  │ │  Database VMs   │ │
│ │  (NSG)    │ │  │ │  (NSG)    │ │  │ │  (NSG)          │ │
│ └───────────┘ │  │ └───────────┘ │  │ └─────────────────┘ │
│               │  │               │  │                     │
│  Subnet:      │  │  Subnet:      │  │  Subnet:            │
│  10.1.1.0/24  │  │  10.2.1.0/24  │  │  10.3.1.0/24        │
└───────────────┘  └───────────────┘  └─────────────────────┘

Traffic Flow:
1. Internet → Azure Firewall → Spoke VMs (inbound)
2. Spoke VMs → Azure Firewall → Internet (outbound)
3. Spoke1 → Azure Firewall → Spoke2 (east-west)
4. On-premises → VPN Gateway → Hub → Spokes (hybrid)

What You'll Learn

  • Virtual Network design and IP address planning
  • VNet peering configuration
  • Azure Firewall deployment and policies
  • User Defined Routes (UDR) for traffic control
  • Network Security Groups (NSGs)
  • Azure Bastion for secure VM access
  • VPN Gateway for hybrid connectivity

Prerequisites

  • Azure subscription with Owner or Contributor access
  • Azure CLI installed and configured
  • Basic understanding of networking concepts

Phase 1: Create the Hub VNet

Step 1.1: Create Resource Group

bash
# Set variables
LOCATION="eastus"
RG_NAME="rg-hubspoke-lab-eastus"

# Create resource group
az group create \
  --name $RG_NAME \
  --location $LOCATION \
  --tags Project=HubSpoke Environment=Lab

echo "Resource group created: $RG_NAME"

Step 1.2: Create Hub Virtual Network

bash
# Create Hub VNet with multiple subnets
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-hub \
  --address-prefix 10.0.0.0/16 \
  --subnet-name AzureFirewallSubnet \
  --subnet-prefix 10.0.1.0/26 \
  --location $LOCATION

# Add Gateway Subnet (required for VPN Gateway)
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-hub \
  --name GatewaySubnet \
  --address-prefix 10.0.2.0/27

# Add Azure Bastion Subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-hub \
  --name AzureBastionSubnet \
  --address-prefix 10.0.3.0/27

# Add Management Subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-hub \
  --name snet-management \
  --address-prefix 10.0.4.0/24

echo "Hub VNet created with subnets"

Step 1.3: Verify Hub VNet

bash
# List all subnets
az network vnet subnet list \
  --resource-group $RG_NAME \
  --vnet-name vnet-hub \
  --output table

Expected output:

Name                  AddressPrefix    PrivateEndpointNetworkPolicies
--------------------  ---------------  -------------------------------
AzureFirewallSubnet   10.0.1.0/26      Disabled
GatewaySubnet         10.0.2.0/27      Disabled
AzureBastionSubnet    10.0.3.0/27      Disabled
snet-management       10.0.4.0/24      Disabled

Phase 2: Create Spoke VNets

Step 2.1: Create Web Tier Spoke (Spoke 1)

bash
# Create Spoke 1 - Web Tier
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-spoke-web \
  --address-prefix 10.1.0.0/16 \
  --subnet-name snet-web \
  --subnet-prefix 10.1.1.0/24 \
  --location $LOCATION

echo "Spoke 1 (Web) VNet created"

Step 2.2: Create App Tier Spoke (Spoke 2)

bash
# Create Spoke 2 - App Tier
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-spoke-app \
  --address-prefix 10.2.0.0/16 \
  --subnet-name snet-app \
  --subnet-prefix 10.2.1.0/24 \
  --location $LOCATION

echo "Spoke 2 (App) VNet created"

Step 2.3: Create Data Tier Spoke (Spoke 3)

bash
# Create Spoke 3 - Data Tier
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-spoke-data \
  --address-prefix 10.3.0.0/16 \
  --subnet-name snet-data \
  --subnet-prefix 10.3.1.0/24 \
  --location $LOCATION

echo "Spoke 3 (Data) VNet created"

Phase 3: Configure VNet Peering

Step 3.1: Peer Hub to Spokes

bash
# Peer Hub to Spoke-Web
az network vnet peering create \
  --resource-group $RG_NAME \
  --name hub-to-spoke-web \
  --vnet-name vnet-hub \
  --remote-vnet vnet-spoke-web \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --allow-gateway-transit

# Peer Hub to Spoke-App
az network vnet peering create \
  --resource-group $RG_NAME \
  --name hub-to-spoke-app \
  --vnet-name vnet-hub \
  --remote-vnet vnet-spoke-app \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --allow-gateway-transit

# Peer Hub to Spoke-Data
az network vnet peering create \
  --resource-group $RG_NAME \
  --name hub-to-spoke-data \
  --vnet-name vnet-hub \
  --remote-vnet vnet-spoke-data \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --allow-gateway-transit

echo "Hub-to-Spoke peerings created"

Step 3.2: Peer Spokes to Hub

bash
# Peer Spoke-Web to Hub
az network vnet peering create \
  --resource-group $RG_NAME \
  --name spoke-web-to-hub \
  --vnet-name vnet-spoke-web \
  --remote-vnet vnet-hub \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --use-remote-gateways false

# Peer Spoke-App to Hub
az network vnet peering create \
  --resource-group $RG_NAME \
  --name spoke-app-to-hub \
  --vnet-name vnet-spoke-app \
  --remote-vnet vnet-hub \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --use-remote-gateways false

# Peer Spoke-Data to Hub
az network vnet peering create \
  --resource-group $RG_NAME \
  --name spoke-data-to-hub \
  --vnet-name vnet-spoke-data \
  --remote-vnet vnet-hub \
  --allow-vnet-access \
  --allow-forwarded-traffic \
  --use-remote-gateways false

echo "Spoke-to-Hub peerings created"

Step 3.3: Verify Peering Status

bash
# Check all peerings
az network vnet peering list \
  --resource-group $RG_NAME \
  --vnet-name vnet-hub \
  --output table

# Should show "Connected" status for all peerings

Phase 4: Deploy Azure Firewall

Step 4.1: Create Public IP for Firewall

bash
# Create Public IP
az network public-ip create \
  --resource-group $RG_NAME \
  --name pip-azfw \
  --sku Standard \
  --allocation-method Static \
  --location $LOCATION

# Get the IP address
FW_PUBLIC_IP=$(az network public-ip show \
  --resource-group $RG_NAME \
  --name pip-azfw \
  --query ipAddress -o tsv)

echo "Firewall Public IP: $FW_PUBLIC_IP"

Step 4.2: Create Firewall Policy

bash
# Create Firewall Policy
az network firewall policy create \
  --resource-group $RG_NAME \
  --name policy-azfw \
  --location $LOCATION \
  --sku Standard

echo "Firewall policy created"

Step 4.3: Create Firewall Policy Rule Collection Groups

bash
# Create Network Rule Collection Group
az network firewall policy rule-collection-group create \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --name NetworkRuleCollectionGroup \
  --priority 200

# Create Application Rule Collection Group
az network firewall policy rule-collection-group create \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --name ApplicationRuleCollectionGroup \
  --priority 300

Step 4.4: Add Network Rules

bash
# Allow spoke-to-spoke traffic
az network firewall policy rule-collection-group collection add-filter-collection \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --rule-collection-group-name NetworkRuleCollectionGroup \
  --name "AllowSpokeToSpoke" \
  --collection-priority 100 \
  --action Allow \
  --rule-name "AllowAllSpokes" \
  --rule-type NetworkRule \
  --source-addresses "10.1.0.0/16" "10.2.0.0/16" "10.3.0.0/16" \
  --destination-addresses "10.1.0.0/16" "10.2.0.0/16" "10.3.0.0/16" \
  --ip-protocols Any \
  --destination-ports "*"

# Allow DNS traffic
az network firewall policy rule-collection-group collection add-filter-collection \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --rule-collection-group-name NetworkRuleCollectionGroup \
  --name "AllowDNS" \
  --collection-priority 110 \
  --action Allow \
  --rule-name "DNS" \
  --rule-type NetworkRule \
  --source-addresses "10.0.0.0/8" \
  --destination-addresses "168.63.129.16" \
  --ip-protocols UDP \
  --destination-ports "53"

Step 4.5: Add Application Rules

bash
# Allow outbound web traffic
az network firewall policy rule-collection-group collection add-filter-collection \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --rule-collection-group-name ApplicationRuleCollectionGroup \
  --name "AllowWeb" \
  --collection-priority 100 \
  --action Allow \
  --rule-name "AllowHTTPS" \
  --rule-type ApplicationRule \
  --source-addresses "10.0.0.0/8" \
  --protocols Https=443 Http=80 \
  --target-fqdns "*"

# Allow Azure services
az network firewall policy rule-collection-group collection add-filter-collection \
  --resource-group $RG_NAME \
  --policy-name policy-azfw \
  --rule-collection-group-name ApplicationRuleCollectionGroup \
  --name "AllowAzureServices" \
  --collection-priority 110 \
  --action Allow \
  --rule-name "AzureServices" \
  --rule-type ApplicationRule \
  --source-addresses "10.0.0.0/8" \
  --protocols Https=443 \
  --fqdn-tags "AzureBackup" "WindowsUpdate" "AzureKubernetesService"

Step 4.6: Deploy Azure Firewall

Long Running Command

This command takes 10-15 minutes to complete. Azure Firewall deployment is slow.

bash
# Create Azure Firewall
az network firewall create \
  --resource-group $RG_NAME \
  --name azfw-hub \
  --location $LOCATION \
  --sku AZFW_VNet \
  --tier Standard \
  --firewall-policy policy-azfw \
  --vnet-name vnet-hub

# Configure Firewall IP
az network firewall ip-config create \
  --resource-group $RG_NAME \
  --firewall-name azfw-hub \
  --name FW-config \
  --public-ip-address pip-azfw \
  --vnet-name vnet-hub

# Get Firewall Private IP
FW_PRIVATE_IP=$(az network firewall show \
  --resource-group $RG_NAME \
  --name azfw-hub \
  --query "ipConfigurations[0].privateIPAddress" -o tsv)

echo "Firewall Private IP: $FW_PRIVATE_IP"

Phase 5: Configure Route Tables (UDR)

Step 5.1: Create Route Table for Spokes

bash
# Create Route Table
az network route-table create \
  --resource-group $RG_NAME \
  --name rt-spoke-to-hub \
  --location $LOCATION \
  --disable-bgp-route-propagation true

# Add route to send all traffic through Firewall
az network route-table route create \
  --resource-group $RG_NAME \
  --route-table-name rt-spoke-to-hub \
  --name to-internet \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

# Add route for spoke-to-spoke via Firewall
az network route-table route create \
  --resource-group $RG_NAME \
  --route-table-name rt-spoke-to-hub \
  --name to-spoke-web \
  --address-prefix 10.1.0.0/16 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

az network route-table route create \
  --resource-group $RG_NAME \
  --route-table-name rt-spoke-to-hub \
  --name to-spoke-app \
  --address-prefix 10.2.0.0/16 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

az network route-table route create \
  --resource-group $RG_NAME \
  --route-table-name rt-spoke-to-hub \
  --name to-spoke-data \
  --address-prefix 10.3.0.0/16 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

Step 5.2: Associate Route Table with Spoke Subnets

bash
# Associate with Web Spoke
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-web \
  --name snet-web \
  --route-table rt-spoke-to-hub

# Associate with App Spoke
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-app \
  --name snet-app \
  --route-table rt-spoke-to-hub

# Associate with Data Spoke
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-data \
  --name snet-data \
  --route-table rt-spoke-to-hub

echo "Route tables associated with all spoke subnets"

Phase 6: Configure Network Security Groups

Step 6.1: Create NSG for Web Tier

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-web \
  --location $LOCATION

# Allow HTTP/HTTPS from Internet (via Firewall)
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name AllowHTTP \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.0.1.0/26" \
  --destination-port-ranges 80 443

# Allow SSH from Bastion subnet
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name AllowSSHFromBastion \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.0.3.0/27" \
  --destination-port-ranges 22

# Associate with Web subnet
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-web \
  --name snet-web \
  --network-security-group nsg-web

Step 6.2: Create NSG for App Tier

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-app \
  --location $LOCATION

# Allow traffic from Web tier only
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-app \
  --name AllowFromWeb \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.1.0.0/16" \
  --destination-port-ranges 8080

# Allow SSH from Bastion
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-app \
  --name AllowSSHFromBastion \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.0.3.0/27" \
  --destination-port-ranges 22

# Associate with App subnet
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-app \
  --name snet-app \
  --network-security-group nsg-app

Step 6.3: Create NSG for Data Tier

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-data \
  --location $LOCATION

# Allow SQL from App tier only
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-data \
  --name AllowSQLFromApp \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.2.0.0/16" \
  --destination-port-ranges 1433

# Allow SSH from Bastion
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-data \
  --name AllowSSHFromBastion \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "10.0.3.0/27" \
  --destination-port-ranges 22

# Associate with Data subnet
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-spoke-data \
  --name snet-data \
  --network-security-group nsg-data

Phase 7: Deploy Azure Bastion

Step 7.1: Create Bastion Public IP

bash
# Create Public IP for Bastion
az network public-ip create \
  --resource-group $RG_NAME \
  --name pip-bastion \
  --sku Standard \
  --allocation-method Static \
  --location $LOCATION

Step 7.2: Deploy Azure Bastion

bash
# Create Azure Bastion
az network bastion create \
  --resource-group $RG_NAME \
  --name bastion-hub \
  --public-ip-address pip-bastion \
  --vnet-name vnet-hub \
  --location $LOCATION \
  --sku Basic

echo "Azure Bastion deployed"

Phase 8: Deploy Test VMs

Step 8.1: Deploy VM in Web Spoke

bash
# Create Web VM
az vm create \
  --resource-group $RG_NAME \
  --name vm-web-01 \
  --vnet-name vnet-spoke-web \
  --subnet snet-web \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-address "" \
  --no-wait

Step 8.2: Deploy VM in App Spoke

bash
# Create App VM
az vm create \
  --resource-group $RG_NAME \
  --name vm-app-01 \
  --vnet-name vnet-spoke-app \
  --subnet snet-app \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-address "" \
  --no-wait

Step 8.3: Deploy VM in Data Spoke

bash
# Create Data VM
az vm create \
  --resource-group $RG_NAME \
  --name vm-data-01 \
  --vnet-name vnet-spoke-data \
  --subnet snet-data \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-address "" \
  --no-wait

# Wait for VMs to be created
echo "Waiting for VMs to be created..."
az vm wait --resource-group $RG_NAME --name vm-web-01 --created
az vm wait --resource-group $RG_NAME --name vm-app-01 --created
az vm wait --resource-group $RG_NAME --name vm-data-01 --created

echo "All VMs created"

Phase 9: Testing and Verification

Step 9.1: Test Connectivity via Bastion

  1. Navigate to Azure Portal → vm-web-01
  2. Click "Connect" → "Bastion"
  3. Enter username: azureuser
  4. Use SSH key or password to connect

Step 9.2: Test Spoke-to-Spoke Connectivity

From vm-web-01 (via Bastion):

bash
# Get private IPs
VM_APP_IP=$(az vm show \
  --resource-group $RG_NAME \
  --name vm-app-01 \
  --show-details \
  --query privateIps -o tsv)

VM_DATA_IP=$(az vm show \
  --resource-group $RG_NAME \
  --name vm-data-01 \
  --show-details \
  --query privateIps -o tsv)

# Test connectivity
echo "App VM IP: $VM_APP_IP"
echo "Data VM IP: $VM_DATA_IP"

Inside the VM:

bash
# Ping App VM (should work via Firewall)
ping 10.2.1.4 -c 4

# Ping Data VM (should work via Firewall)
ping 10.3.1.4 -c 4

# Test internet access (should work via Firewall)
curl -I https://www.microsoft.com

Step 9.3: Verify Firewall Logs

bash
# Enable diagnostics (if not enabled)
az monitor diagnostic-settings create \
  --resource $(az network firewall show -g $RG_NAME -n azfw-hub --query id -o tsv) \
  --name "fw-diagnostics" \
  --logs '[{"category": "AzureFirewallNetworkRule", "enabled": true}, {"category": "AzureFirewallApplicationRule", "enabled": true}]' \
  --workspace $(az monitor log-analytics workspace show -g $RG_NAME -n law-hubspoke --query id -o tsv)

Architecture Summary

ComponentPurposeIP Range
Hub VNetCentral connectivity10.0.0.0/16
Azure FirewallSecurity & routing10.0.1.0/26
VPN GatewayHybrid connectivity10.0.2.0/27
Azure BastionSecure VM access10.0.3.0/27
Spoke-WebWeb tier VMs10.1.0.0/16
Spoke-AppApplication tier VMs10.2.0.0/16
Spoke-DataDatabase tier VMs10.3.0.0/16

Cleanup

Important

Delete resources to avoid charges!

bash
# Delete resource group and all resources
az group delete --name $RG_NAME --yes --no-wait

echo "Cleanup initiated. Resources will be deleted in a few minutes."

Key Takeaways

  1. Hub-Spoke Pattern: Centralized control, distributed workloads
  2. Azure Firewall: L4-L7 filtering, FQDN-based rules, threat intelligence
  3. UDR: Forces traffic through security appliances
  4. VNet Peering: Low-latency, high-bandwidth connectivity
  5. Azure Bastion: Eliminates public IP exposure on VMs
  6. NSGs: Defense in depth at subnet level

Next Steps

  • Add VPN Gateway for hybrid connectivity
  • Implement Azure DDoS Protection
  • Add Network Watcher for monitoring
  • Configure Azure Monitor for network analytics

Released under the MIT License.