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

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.

Azure Infrastructure Experts

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