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 tableExpected 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 DisabledPhase 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 peeringsPhase 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 300Step 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_IPStep 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-webStep 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-appStep 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-dataPhase 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 $LOCATIONStep 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-waitStep 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-waitStep 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
- Navigate to Azure Portal → vm-web-01
- Click "Connect" → "Bastion"
- Enter username:
azureuser - 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.comStep 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
| Component | Purpose | IP Range |
|---|---|---|
| Hub VNet | Central connectivity | 10.0.0.0/16 |
| Azure Firewall | Security & routing | 10.0.1.0/26 |
| VPN Gateway | Hybrid connectivity | 10.0.2.0/27 |
| Azure Bastion | Secure VM access | 10.0.3.0/27 |
| Spoke-Web | Web tier VMs | 10.1.0.0/16 |
| Spoke-App | Application tier VMs | 10.2.0.0/16 |
| Spoke-Data | Database tier VMs | 10.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
- Hub-Spoke Pattern: Centralized control, distributed workloads
- Azure Firewall: L4-L7 filtering, FQDN-based rules, threat intelligence
- UDR: Forces traffic through security appliances
- VNet Peering: Low-latency, high-bandwidth connectivity
- Azure Bastion: Eliminates public IP exposure on VMs
- 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