Azure VNet Network Segmentation: NSGs, ASGs, and Hub-Spoke Architecture

Learn how to implement network segmentation in Azure using Virtual Networks, subnets, Network Security Groups, Application Security Groups, Azure Firewall, and hub-spoke topology.

18 min readUpdated 2026-01-13

Want us to handle this for you?

Get expert help →

Network segmentation is fundamental to Azure security—isolating workloads limits the blast radius of breaches and enables granular access control. This guide covers implementing defense-in-depth network architecture using Virtual Networks, subnets, Network Security Groups, Application Security Groups, Azure Firewall, and hub-spoke topology.

This article is part of our comprehensive cloud security tips guide, focusing specifically on Azure network security architecture.

Overview

Azure network segmentation involves multiple layers:

  • Virtual Networks (VNets): Isolated network boundaries
  • Subnets: Logical divisions within VNets
  • Network Security Groups (NSGs): Layer 3/4 traffic filtering
  • Application Security Groups (ASGs): Logical grouping for NSG rules
  • Azure Firewall: Layer 7 firewall with threat intelligence
  • Route Tables (UDRs): Custom traffic routing
  • VNet Peering: Connecting VNets together

Prerequisites

Before implementing network segmentation:

  • Azure subscription with Network Contributor role
  • Planned IP address scheme that doesn't overlap with on-premises
  • Azure CLI (2.50.0+) or Azure PowerShell
  • Understanding of TCP/IP networking concepts
  • Documented security requirements and traffic flows

Part 1: Virtual Network Design

Address Space Planning

Enterprise Address Space Example (10.0.0.0/8):

Hub VNet:     10.0.0.0/16
├── Gateway Subnet:        10.0.0.0/24
├── Firewall Subnet:       10.0.1.0/24
├── Bastion Subnet:        10.0.2.0/26
├── Management Subnet:     10.0.3.0/24
└── DNS Subnet:            10.0.4.0/24

Spoke 1 (Production):      10.1.0.0/16
├── Web Tier:              10.1.1.0/24
├── App Tier:              10.1.2.0/24
├── Database Tier:         10.1.3.0/24
└── Private Endpoints:     10.1.4.0/24

Spoke 2 (Development):     10.2.0.0/16
├── Web Tier:              10.2.1.0/24
├── App Tier:              10.2.2.0/24
└── Database Tier:         10.2.3.0/24

Spoke 3 (Staging):         10.3.0.0/16
└── ...

Create Hub VNet

# Create resource group for networking
az group create \
  --name rg-networking-hub \
  --location eastus

# Create hub VNet
az network vnet create \
  --name vnet-hub \
  --resource-group rg-networking-hub \
  --location eastus \
  --address-prefix 10.0.0.0/16 \
  --tags environment=hub purpose=networking

# Create hub subnets
az network vnet subnet create \
  --name GatewaySubnet \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --address-prefix 10.0.0.0/24

az network vnet subnet create \
  --name AzureFirewallSubnet \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --address-prefix 10.0.1.0/24

az network vnet subnet create \
  --name AzureBastionSubnet \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --address-prefix 10.0.2.0/26

az network vnet subnet create \
  --name snet-management \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --address-prefix 10.0.3.0/24

Create Spoke VNets

# Create production spoke resource group
az group create \
  --name rg-networking-prod \
  --location eastus

# Create production spoke VNet
az network vnet create \
  --name vnet-spoke-prod \
  --resource-group rg-networking-prod \
  --location eastus \
  --address-prefix 10.1.0.0/16 \
  --tags environment=production

# Create tiered subnets
az network vnet subnet create \
  --name snet-web \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --address-prefix 10.1.1.0/24

az network vnet subnet create \
  --name snet-app \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --address-prefix 10.1.2.0/24

az network vnet subnet create \
  --name snet-database \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --address-prefix 10.1.3.0/24

az network vnet subnet create \
  --name snet-private-endpoints \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --address-prefix 10.1.4.0/24 \
  --disable-private-endpoint-network-policies true

Part 2: Network Security Groups (NSGs)

Create NSGs for Each Subnet Tier

# Create NSG for web tier
az network nsg create \
  --name nsg-web \
  --resource-group rg-networking-prod \
  --location eastus

# Create NSG for app tier
az network nsg create \
  --name nsg-app \
  --resource-group rg-networking-prod \
  --location eastus

# Create NSG for database tier
az network nsg create \
  --name nsg-database \
  --resource-group rg-networking-prod \
  --location eastus

Configure Web Tier NSG Rules

# Allow HTTPS from internet (via Application Gateway)
az network nsg rule create \
  --nsg-name nsg-web \
  --resource-group rg-networking-prod \
  --name AllowHTTPS \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-port-ranges 443 \
  --description "Allow HTTPS from internet"

# Allow HTTP for redirect (optional)
az network nsg rule create \
  --nsg-name nsg-web \
  --resource-group rg-networking-prod \
  --name AllowHTTP \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-port-ranges 80 \
  --description "Allow HTTP for HTTPS redirect"

# Allow Azure Load Balancer health probes
az network nsg rule create \
  --nsg-name nsg-web \
  --resource-group rg-networking-prod \
  --name AllowAzureLB \
  --priority 120 \
  --direction Inbound \
  --access Allow \
  --protocol "*" \
  --source-address-prefixes AzureLoadBalancer \
  --destination-port-ranges "*" \
  --description "Allow Azure Load Balancer probes"

# Deny all other inbound traffic
az network nsg rule create \
  --nsg-name nsg-web \
  --resource-group rg-networking-prod \
  --name DenyAllInbound \
  --priority 4096 \
  --direction Inbound \
  --access Deny \
  --protocol "*" \
  --source-address-prefixes "*" \
  --destination-port-ranges "*" \
  --description "Deny all other inbound"

Configure App Tier NSG Rules

# Allow traffic only from web tier
az network nsg rule create \
  --nsg-name nsg-app \
  --resource-group rg-networking-prod \
  --name AllowFromWebTier \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.1.1.0/24 \
  --destination-port-ranges 8080 8443 \
  --description "Allow from web tier"

# Allow management from hub
az network nsg rule create \
  --nsg-name nsg-app \
  --resource-group rg-networking-prod \
  --name AllowManagement \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.3.0/24 \
  --destination-port-ranges 22 3389 \
  --description "Allow SSH/RDP from management subnet"

# Deny all other inbound
az network nsg rule create \
  --nsg-name nsg-app \
  --resource-group rg-networking-prod \
  --name DenyAllInbound \
  --priority 4096 \
  --direction Inbound \
  --access Deny \
  --protocol "*" \
  --source-address-prefixes "*" \
  --destination-port-ranges "*"

Configure Database Tier NSG Rules

# Allow SQL from app tier only
az network nsg rule create \
  --nsg-name nsg-database \
  --resource-group rg-networking-prod \
  --name AllowSQLFromAppTier \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.1.2.0/24 \
  --destination-port-ranges 1433 3306 5432 \
  --description "Allow SQL/MySQL/PostgreSQL from app tier"

# Deny direct internet access
az network nsg rule create \
  --nsg-name nsg-database \
  --resource-group rg-networking-prod \
  --name DenyInternet \
  --priority 4000 \
  --direction Outbound \
  --access Deny \
  --protocol "*" \
  --destination-address-prefixes Internet \
  --destination-port-ranges "*" \
  --description "Deny direct internet access"

# Deny all other inbound
az network nsg rule create \
  --nsg-name nsg-database \
  --resource-group rg-networking-prod \
  --name DenyAllInbound \
  --priority 4096 \
  --direction Inbound \
  --access Deny \
  --protocol "*" \
  --source-address-prefixes "*" \
  --destination-port-ranges "*"

Associate NSGs with Subnets

# Associate NSGs with subnets
az network vnet subnet update \
  --name snet-web \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --network-security-group nsg-web

az network vnet subnet update \
  --name snet-app \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --network-security-group nsg-app

az network vnet subnet update \
  --name snet-database \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --network-security-group nsg-database

Part 3: Application Security Groups (ASGs)

ASGs simplify NSG rule management by grouping VMs logically.

Create ASGs

# Create ASGs for each application role
az network asg create \
  --name asg-webservers \
  --resource-group rg-networking-prod \
  --location eastus

az network asg create \
  --name asg-appservers \
  --resource-group rg-networking-prod \
  --location eastus

az network asg create \
  --name asg-databases \
  --resource-group rg-networking-prod \
  --location eastus

az network asg create \
  --name asg-jumpboxes \
  --resource-group rg-networking-prod \
  --location eastus

Create NSG Rules Using ASGs

# Create a new NSG for ASG-based rules
az network nsg create \
  --name nsg-asg-based \
  --resource-group rg-networking-prod \
  --location eastus

# Allow web servers to communicate with app servers
az network nsg rule create \
  --nsg-name nsg-asg-based \
  --resource-group rg-networking-prod \
  --name AllowWebToApp \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-asgs asg-webservers \
  --destination-asgs asg-appservers \
  --destination-port-ranges 8080 8443 \
  --description "Allow web servers to reach app servers"

# Allow app servers to communicate with databases
az network nsg rule create \
  --nsg-name nsg-asg-based \
  --resource-group rg-networking-prod \
  --name AllowAppToDatabase \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-asgs asg-appservers \
  --destination-asgs asg-databases \
  --destination-port-ranges 1433 3306 5432 \
  --description "Allow app servers to reach databases"

# Allow jumpboxes to manage all servers
az network nsg rule create \
  --nsg-name nsg-asg-based \
  --resource-group rg-networking-prod \
  --name AllowJumpboxManagement \
  --priority 120 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-asgs asg-jumpboxes \
  --destination-port-ranges 22 3389 \
  --description "Allow jumpbox SSH/RDP access"

Associate VM NICs with ASGs

# When creating a VM, associate its NIC with an ASG
az network nic create \
  --name nic-webserver-01 \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --subnet snet-web \
  --application-security-groups asg-webservers

# Or update existing NIC
az network nic ip-config update \
  --name ipconfig1 \
  --nic-name nic-webserver-01 \
  --resource-group rg-networking-prod \
  --application-security-groups asg-webservers

Part 4: Hub-Spoke Topology with VNet Peering

Create VNet Peerings

# Get VNet resource IDs
HUB_VNET_ID=$(az network vnet show \
  --name vnet-hub \
  --resource-group rg-networking-hub \
  --query id -o tsv)

SPOKE_VNET_ID=$(az network vnet show \
  --name vnet-spoke-prod \
  --resource-group rg-networking-prod \
  --query id -o tsv)

# Create peering from hub to spoke
az network vnet peering create \
  --name hub-to-spoke-prod \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --remote-vnet $SPOKE_VNET_ID \
  --allow-vnet-access true \
  --allow-forwarded-traffic true \
  --allow-gateway-transit true

# Create peering from spoke to hub
az network vnet peering create \
  --name spoke-prod-to-hub \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --remote-vnet $HUB_VNET_ID \
  --allow-vnet-access true \
  --allow-forwarded-traffic true \
  --use-remote-gateways false  # Set to true when VPN gateway exists

Verify Peering Status

# Check peering status
az network vnet peering show \
  --name hub-to-spoke-prod \
  --resource-group rg-networking-hub \
  --vnet-name vnet-hub \
  --query peeringState -o tsv
# Should return: Connected

az network vnet peering show \
  --name spoke-prod-to-hub \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --query peeringState -o tsv
# Should return: Connected

Part 5: Azure Firewall Deployment

Deploy Azure Firewall in Hub

# Create public IP for firewall
az network public-ip create \
  --name pip-azfw \
  --resource-group rg-networking-hub \
  --location eastus \
  --allocation-method Static \
  --sku Standard

# Create Azure Firewall
az network firewall create \
  --name azfw-hub \
  --resource-group rg-networking-hub \
  --location eastus \
  --sku AZFW_VNet \
  --tier Premium \
  --threat-intel-mode Deny

# Configure firewall with public IP
az network firewall ip-config create \
  --firewall-name azfw-hub \
  --name fw-ipconfig \
  --resource-group rg-networking-hub \
  --public-ip-address pip-azfw \
  --vnet-name vnet-hub

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

echo "Firewall Private IP: $FW_PRIVATE_IP"

Create Firewall Policy

# Create firewall policy
az network firewall policy create \
  --name policy-azfw-hub \
  --resource-group rg-networking-hub \
  --location eastus \
  --sku Premium \
  --threat-intel-mode Deny \
  --idps-mode Deny

# Create rule collection group
az network firewall policy rule-collection-group create \
  --name rcg-application-rules \
  --policy-name policy-azfw-hub \
  --resource-group rg-networking-hub \
  --priority 100

# Add application rules (allow outbound web access)
az network firewall policy rule-collection-group collection add-filter-collection \
  --name rc-allow-web \
  --policy-name policy-azfw-hub \
  --resource-group rg-networking-hub \
  --rcg-name rcg-application-rules \
  --collection-priority 100 \
  --action Allow \
  --rule-name "AllowMicrosoftUpdates" \
  --rule-type ApplicationRule \
  --source-addresses "10.1.0.0/16" \
  --protocols https=443 \
  --fqdn-tags "WindowsUpdate" "MicrosoftActiveProtectionService"

# Add network rules (allow DNS)
az network firewall policy rule-collection-group collection add-filter-collection \
  --name rc-allow-dns \
  --policy-name policy-azfw-hub \
  --resource-group rg-networking-hub \
  --rcg-name rcg-application-rules \
  --collection-priority 110 \
  --action Allow \
  --rule-name "AllowDNS" \
  --rule-type NetworkRule \
  --source-addresses "10.1.0.0/16" \
  --destination-addresses "168.63.129.16" \
  --destination-ports 53 \
  --ip-protocols UDP TCP

# Associate policy with firewall
az network firewall update \
  --name azfw-hub \
  --resource-group rg-networking-hub \
  --firewall-policy policy-azfw-hub

Create User Defined Routes (UDRs)

Route spoke traffic through the firewall:

# Create route table for spokes
az network route-table create \
  --name rt-spoke-to-firewall \
  --resource-group rg-networking-prod \
  --location eastus \
  --disable-bgp-route-propagation true

# Default route to firewall
az network route-table route create \
  --route-table-name rt-spoke-to-firewall \
  --resource-group rg-networking-prod \
  --name route-to-firewall \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

# Route to other spokes via firewall
az network route-table route create \
  --route-table-name rt-spoke-to-firewall \
  --resource-group rg-networking-prod \
  --name route-to-spoke-dev \
  --address-prefix 10.2.0.0/16 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

# Associate route table with spoke subnets
az network vnet subnet update \
  --name snet-web \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --route-table rt-spoke-to-firewall

az network vnet subnet update \
  --name snet-app \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --route-table rt-spoke-to-firewall

az network vnet subnet update \
  --name snet-database \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --route-table rt-spoke-to-firewall

Part 6: Terraform Configuration

Complete infrastructure-as-code implementation:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.85"
    }
  }
}

provider "azurerm" {
  features {}
}

# Variables
variable "location" {
  default = "eastus"
}

variable "hub_address_space" {
  default = "10.0.0.0/16"
}

variable "spoke_prod_address_space" {
  default = "10.1.0.0/16"
}

# Resource Groups
resource "azurerm_resource_group" "hub" {
  name     = "rg-networking-hub"
  location = var.location
}

resource "azurerm_resource_group" "spoke_prod" {
  name     = "rg-networking-prod"
  location = var.location
}

# Hub VNet
resource "azurerm_virtual_network" "hub" {
  name                = "vnet-hub"
  resource_group_name = azurerm_resource_group.hub.name
  location            = var.location
  address_space       = [var.hub_address_space]
}

resource "azurerm_subnet" "firewall" {
  name                 = "AzureFirewallSubnet"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "bastion" {
  name                 = "AzureBastionSubnet"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = ["10.0.2.0/26"]
}

resource "azurerm_subnet" "management" {
  name                 = "snet-management"
  resource_group_name  = azurerm_resource_group.hub.name
  virtual_network_name = azurerm_virtual_network.hub.name
  address_prefixes     = ["10.0.3.0/24"]
}

# Spoke VNet
resource "azurerm_virtual_network" "spoke_prod" {
  name                = "vnet-spoke-prod"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  location            = var.location
  address_space       = [var.spoke_prod_address_space]
}

resource "azurerm_subnet" "web" {
  name                 = "snet-web"
  resource_group_name  = azurerm_resource_group.spoke_prod.name
  virtual_network_name = azurerm_virtual_network.spoke_prod.name
  address_prefixes     = ["10.1.1.0/24"]
}

resource "azurerm_subnet" "app" {
  name                 = "snet-app"
  resource_group_name  = azurerm_resource_group.spoke_prod.name
  virtual_network_name = azurerm_virtual_network.spoke_prod.name
  address_prefixes     = ["10.1.2.0/24"]
}

resource "azurerm_subnet" "database" {
  name                 = "snet-database"
  resource_group_name  = azurerm_resource_group.spoke_prod.name
  virtual_network_name = azurerm_virtual_network.spoke_prod.name
  address_prefixes     = ["10.1.3.0/24"]
}

# NSGs
resource "azurerm_network_security_group" "web" {
  name                = "nsg-web"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  location            = var.location

  security_rule {
    name                       = "AllowHTTPS"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyAllInbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_security_group" "app" {
  name                = "nsg-app"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  location            = var.location

  security_rule {
    name                       = "AllowFromWebTier"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_ranges    = ["8080", "8443"]
    source_address_prefix      = "10.1.1.0/24"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyAllInbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_security_group" "database" {
  name                = "nsg-database"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  location            = var.location

  security_rule {
    name                       = "AllowSQLFromAppTier"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_ranges    = ["1433", "3306", "5432"]
    source_address_prefix      = "10.1.2.0/24"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyInternet"
    priority                   = 4000
    direction                  = "Outbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "Internet"
  }
}

# NSG Associations
resource "azurerm_subnet_network_security_group_association" "web" {
  subnet_id                 = azurerm_subnet.web.id
  network_security_group_id = azurerm_network_security_group.web.id
}

resource "azurerm_subnet_network_security_group_association" "app" {
  subnet_id                 = azurerm_subnet.app.id
  network_security_group_id = azurerm_network_security_group.app.id
}

resource "azurerm_subnet_network_security_group_association" "database" {
  subnet_id                 = azurerm_subnet.database.id
  network_security_group_id = azurerm_network_security_group.database.id
}

# VNet Peering
resource "azurerm_virtual_network_peering" "hub_to_spoke" {
  name                         = "hub-to-spoke-prod"
  resource_group_name          = azurerm_resource_group.hub.name
  virtual_network_name         = azurerm_virtual_network.hub.name
  remote_virtual_network_id    = azurerm_virtual_network.spoke_prod.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = true
}

resource "azurerm_virtual_network_peering" "spoke_to_hub" {
  name                         = "spoke-prod-to-hub"
  resource_group_name          = azurerm_resource_group.spoke_prod.name
  virtual_network_name         = azurerm_virtual_network.spoke_prod.name
  remote_virtual_network_id    = azurerm_virtual_network.hub.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  use_remote_gateways          = false
}

# Azure Firewall
resource "azurerm_public_ip" "firewall" {
  name                = "pip-azfw"
  resource_group_name = azurerm_resource_group.hub.name
  location            = var.location
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_firewall" "hub" {
  name                = "azfw-hub"
  resource_group_name = azurerm_resource_group.hub.name
  location            = var.location
  sku_name            = "AZFW_VNet"
  sku_tier            = "Premium"
  threat_intel_mode   = "Deny"

  ip_configuration {
    name                 = "fw-ipconfig"
    subnet_id            = azurerm_subnet.firewall.id
    public_ip_address_id = azurerm_public_ip.firewall.id
  }
}

# Route Table
resource "azurerm_route_table" "spoke_to_firewall" {
  name                = "rt-spoke-to-firewall"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  location            = var.location

  route {
    name                   = "route-to-firewall"
    address_prefix         = "0.0.0.0/0"
    next_hop_type          = "VirtualAppliance"
    next_hop_in_ip_address = azurerm_firewall.hub.ip_configuration[0].private_ip_address
  }
}

resource "azurerm_subnet_route_table_association" "web" {
  subnet_id      = azurerm_subnet.web.id
  route_table_id = azurerm_route_table.spoke_to_firewall.id
}

resource "azurerm_subnet_route_table_association" "app" {
  subnet_id      = azurerm_subnet.app.id
  route_table_id = azurerm_route_table.spoke_to_firewall.id
}

Best Practices Summary

  1. Plan address space carefully - Avoid overlaps with on-premises and other VNets
  2. Use subnet-level NSGs - Apply to subnets rather than individual NICs
  3. Implement deny-by-default - Explicit allow rules only
  4. Leverage ASGs - Simplify rule management for dynamic workloads
  5. Centralize security in hub - Use Azure Firewall for inspection
  6. Log everything - Enable NSG flow logs and firewall diagnostics
  7. Test connectivity - Use Network Watcher for verification
  8. Document thoroughly - Maintain network diagrams and rule documentation

Troubleshooting

Issue: VMs Cannot Communicate Across Peered VNets

# Verify peering status
az network vnet peering show \
  --name spoke-prod-to-hub \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod

# Check effective routes
az network nic show-effective-route-table \
  --name nic-vm-01 \
  --resource-group rg-networking-prod

Issue: Traffic Not Reaching Firewall

# Verify route table association
az network vnet subnet show \
  --name snet-web \
  --resource-group rg-networking-prod \
  --vnet-name vnet-spoke-prod \
  --query routeTable.id

# Check effective security rules
az network nic list-effective-nsg \
  --name nic-vm-01 \
  --resource-group rg-networking-prod

Frequently Asked Questions

Find answers to common questions

Network Security Groups (NSGs) are stateful packet filters operating at layers 3 and 4, applied to subnets or NICs, filtering based on IP addresses, ports, and protocols. Azure Firewall is a managed, cloud-based network security service that operates at layers 3-7, providing URL filtering, threat intelligence, TLS inspection, and centralized logging. Use NSGs for basic subnet isolation and Azure Firewall for advanced threat protection, centralized policy management, and application-layer filtering.

Plan your address space to avoid overlap with on-premises networks or other VNets you may peer with. Use RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). Reserve larger blocks than currently needed (e.g., /16 for hub, /16 for each spoke) to allow growth. Segment into subnets based on workload tiers (web, app, database) and keep consistent naming. Document all allocations in a central IPAM (IP Address Management) system.

Hub-spoke topology centralizes shared services (firewalls, VPN gateways, monitoring) in a hub VNet, with workload VNets (spokes) peered to the hub. Use this pattern when you have: multiple workloads requiring shared connectivity, centralized security requirements, on-premises connectivity needs, or when you want to isolate workloads while maintaining central control. It reduces costs by sharing infrastructure and simplifies network management.

By default, VNet peering is non-transitive—spoke A cannot reach spoke B through the hub unless you explicitly configure it. To enable spoke-to-spoke communication, you must: 1) Enable 'Allow forwarded traffic' on the peerings, 2) Deploy a Network Virtual Appliance (NVA) or Azure Firewall in the hub, and 3) Configure User Defined Routes (UDRs) on spoke subnets to route traffic through the hub. This design gives you central visibility and control over all east-west traffic.

ASGs let you group VMs by application function (web servers, databases, etc.) regardless of their IP addresses. Instead of managing rules with IP addresses that change, you create rules like 'Allow Web-ASG to access Database-ASG on port 1433.' When VMs scale in/out or get new IPs, the rules automatically apply based on ASG membership. This dramatically reduces rule complexity and maintenance, especially in dynamic environments.

Azure Infrastructure Experts

Comprehensive Azure management including architecture, migration, security, and 24/7 operations.