☁️ Azure

Azure Networking: VNets, NSG & ExpressRoute

📅 July 21, 202611 min readITVedas

A precise mapping from on-prem networking to Azure: VNet address planning, NSG rule evaluation order, peering transitivity, and ExpressRoute vs VPN tradeoffs.

ADVANCED
⏱ 11 min read
Prerequisites:
Key Facts
  • NSG rules are evaluated in priority order (lowest number first) and processing stops at the first match — there is no "most specific wins" logic like some on-prem ACLs
  • VNet peering and global peering are NOT transitive: if A is peered to B and B is peered to C, A cannot reach C through B without a separate mechanism
  • Azure reserves 5 IP addresses in every subnet (network address, default gateway, two for Azure DNS mapping, and the broadcast address), so a /24 yields 251 usable addresses, not 254
  • ExpressRoute traffic never touches the public internet — it rides a private connection through a connectivity provider into Microsoft's edge routers, while Site-to-Site VPN Gateway tunnels IPsec over the public internet

VNets and subnets: rethinking physical topology

A Virtual Network (VNet) is not a VLAN with a routing appliance bolted on — it's a software-defined boundary with no physical topology underneath it at all. There's no spanning tree, no trunk ports, no physical uplink to reason about, and no need to worry about broadcast domains in the Ethernet sense. Azure's software-defined network fabric handles packet delivery between subnets within a VNet automatically: every VNet has an implicit, non-removable system route table that routes traffic between all subnets in that VNet with next hop type VnetLocal. You don't configure inter-subnet routing — it exists the moment the subnets exist.

What you do control is address space. A VNet is assigned one or more CIDR blocks (RFC 1918 space is typical, though Azure will let you use non-RFC1918 ranges — avoid this unless you have a documented reason) and subnets are carved out of that space. The planning discipline that matters here is different from on-prem: because VNets frequently get peered together, and because peered address spaces must not overlap, you need to allocate ranges centrally across your subscriptions and regions before anyone spins up a VNet, not after. A common failure mode is teams provisioning VNets independently, each grabbing 10.0.0.0/16, and then discovering six months later that two business units can't be peered without a full re-IP.

Every subnet loses 5 addresses to Azure regardless of size: the network address, the default gateway (first usable address), two addresses reserved for Azure's internal DNS mapping, and the broadcast address. A /24 gives you 251 usable host addresses, not 254 — undersizing subnets on the assumption of on-prem math is a recurring source of "why did my address pool run out" tickets.

Subnet delegation is Azure-specific and has no real on-prem analog. Delegating a subnet (az network vnet subnet update --delegations Microsoft.Web/serverFarms) hands control of that subnet to a specific PaaS service — App Service (regional VNet integration), Azure NetApp Files, SQL Managed Instance, Container Instances — so the service can inject its own resources into the subnet. A subnet can carry exactly one delegation, and a delegated subnet generally can't also host arbitrary VMs. Three subnet names are also reserved by convention and required by specific Azure services: GatewaySubnet (minimum /27 recommended for VPN/ExpressRoute gateways, though /29 technically works for smaller gateway SKUs), AzureFirewallSubnet (minimum /26), and AzureBastionSubnet (minimum /26). Get these sizes wrong at creation time and you'll be rebuilding the VNet, since gateway subnets can't easily be resized once a gateway is deployed into them.

Network Security Groups: rule evaluation and priority

NSGs are the Azure-native stateful packet filter, and the single most commonly misunderstood mechanic is rule evaluation order. Every NSG rule has a priority value between 100 and 4096. Azure evaluates rules in ascending priority order — lowest number first — and processing stops at the first rule that matches the traffic. There is no "most specific rule wins" behavior the way some on-prem ACL implementations work; a broad allow rule at priority 100 will shadow a narrow deny rule at priority 200 even if the deny is more specific. Rule ordering is a design decision, not an afterthought.

Every NSG ships with default rules you cannot delete, only override with higher-priority (lower-number) custom rules:

Inbound:
  65000  AllowVnetInBound        Allow  (VirtualNetwork -> VirtualNetwork)
  65001  AllowAzureLoadBalancerInBound  Allow  (AzureLoadBalancer -> Any)
  65500  DenyAllInBound          Deny   (Any -> Any)

Outbound:
  65000  AllowVnetOutBound       Allow  (VirtualNetwork -> VirtualNetwork)
  65001  AllowInternetOutBound   Allow  (Any -> Internet)
  65500  DenyAllOutBound         Deny   (Any -> Any)

This means, out of the box, all traffic within a VNet (including across peered VNets, since the VirtualNetwork service tag spans peering) is allowed, and all outbound internet traffic is allowed, but nothing unsolicited from the internet gets in. NSGs are stateful: if outbound traffic is permitted, the corresponding inbound return traffic is automatically allowed regardless of inbound rules, and vice versa — you don't write mirrored rule pairs like you would on a stateless ACL.

NSGs can be associated at two levels: the subnet, and the individual NIC. Both can be populated simultaneously, and the evaluation order depends on direction. For inbound traffic, Azure evaluates the subnet-associated NSG first, then the NIC-associated NSG — traffic must clear both to reach the VM. For outbound traffic, it's reversed: the NIC NSG is evaluated first, then the subnet NSG. A packet only reaches its destination if both NSGs, in the appropriate order for that direction, allow it. A common design pattern is a subnet NSG that enforces broad security-team-owned baseline rules (deny by default, allow only expected zones) and a NIC NSG that allows app-specific ports — but if either layer denies the traffic, it's dropped, so debugging "why can't this VM talk to that VM" requires checking both, on both ends of the conversation.

az network nsg rule create \
  --resource-group rg-network \
  --nsg-name nsg-app-subnet \
  --name Allow-Https-From-Hub \
  --priority 200 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.10.0.0/24 \
  --destination-port-ranges 443

VNet peering: global, regional, and the transitivity trap

VNet peering connects two VNets at the Azure fabric level — traffic between peered VNets stays on Microsoft's backbone, never traverses the public internet, and gets full bandwidth of the VM SKU rather than being capped like a gateway connection. There are two flavors: regional peering (VNets in the same region) and global peering (VNets in different regions). Functionally they behave the same way — both establish direct routes between the address spaces — but global peering has historically carried per-GB data transfer charges that regional peering within the same region doesn't, and both require non-overlapping address spaces, which is the most common reason a peering request fails outright.

The fact that trips up nearly everyone coming from on-prem routing: VNet peering is not transitive. If VNet A is peered to VNet B, and VNet B is peered to VNet C, resources in A cannot reach resources in C through B — even though B can reach both. Each peering relationship is a distinct, non-inherited route; Azure does not propagate reachability across a peering chain the way BGP would propagate a route across an AS chain if you let it. This is a deliberate design choice for blast-radius containment, not a bug or a missing route you can just add.

There are three standard ways to work around non-transitivity:

For anything beyond a modest hub-and-spoke, Azure Virtual WAN is the managed alternative — it operates as a Microsoft-managed transitive hub, so spoke-to-spoke and spoke-to-branch routing is automatic without you owning route tables or NVAs.

az network vnet peering create \
  --name hub-to-spoke1 \
  --resource-group rg-network \
  --vnet-name vnet-hub \
  --remote-vnet vnet-spoke1 \
  --allow-vnet-access true \
  --allow-gateway-transit true

ExpressRoute vs Site-to-Site VPN Gateway

Both connect on-prem networks to Azure VNets, and both terminate on a gateway subnet, but the transport underneath is fundamentally different, and that difference drives the cost, latency, and SLA tradeoffs.

A Site-to-Site VPN Gateway builds an IPsec/IKE tunnel over the public internet — the same mechanism you'd use between two on-prem firewalls. It's provisioned in hours, billed per gateway-hour on the SKU (Basic through VpnGw5) plus egress, and throughput scales with SKU from roughly 100 Mbps (Basic, deprecated for new deployments) up to several Gbps on the higher AZ SKUs. SLA is 99.9-99.95% depending on active-active/zone-redundant configuration. Latency and jitter are whatever the internet path between your edge and Azure's happens to be that day — not something you can contract for.

ExpressRoute is a private, dedicated Layer 3 connection established through a connectivity provider (a carrier hotel / peering location) or via ExpressRoute Direct if you're bringing your own fiber into Microsoft's edge. Traffic never enters the public internet. Circuit bandwidths run from 50 Mbps up to 10 Gbps for standard circuits, or up to 100 Gbps aggregate with ExpressRoute Direct. SLA is 99.95% for a single circuit and higher with dual circuits in an active-active design. Provisioning takes weeks, not hours — you're waiting on a connectivity provider to physically cross-connect you, and pricing includes both the circuit (metered or unlimited data plans) and often the provider's local loop.

The practical decision: ExpressRoute is the right call when you have sustained, latency-sensitive, or high-volume hybrid traffic — a data center lift-and-shift, real-time replication, or a compliance requirement that traffic never touch the public internet. A VPN Gateway is the right default for smaller workloads, dev/test hybrid connectivity, sites that don't justify a dedicated circuit, or as a resilient failover path for an ExpressRoute circuit — a well-known pattern is running both, with the VPN as backup, since ExpressRoute circuits do fail (fiber cuts, provider outages) and a coexisting VPN gateway can fail traffic over automatically when configured with matching BGP preferences.

One nuance worth flagging: ExpressRoute has two peering types that matter here — Private Peering reaches your VNets directly, while Microsoft Peering reaches Microsoft 365 and public PaaS endpoints over the private circuit instead of the internet. Neither peering type, by itself, gets you into a specific PaaS resource's private IP — that's what Private Link is for.

Before Private Link, the option for keeping PaaS traffic off the public internet was VNet Service Endpoints — extending your VNet's identity to a service (e.g., Microsoft.Storage) so that traffic from that subnet was recognized and allowed by the service's firewall, and routed over the Azure backbone instead of the internet. The catch: the PaaS resource still had a public IP and public DNS name. Service Endpoints authorize a source, they don't relocate the destination.

A Private Endpoint does something structurally different: it provisions a NIC with a real private IP address, from your VNet's own address space, that maps directly to a specific PaaS resource instance (a specific storage account, a specific SQL server, a specific Key Vault) via Azure Private Link. From the VNet's perspective, the PaaS service now looks like just another host on the network. Because it's a genuine private IP in your address space, it's reachable from anywhere your VNet is reachable from — including on-premises over ExpressRoute Private Peering or a Site-to-Site VPN — without any traffic ever touching the public internet or needing NAT.

The operational requirement that catches people out is DNS. Private Endpoints don't change the PaaS resource's FQDN; they rely on DNS resolution being overridden so that the existing public FQDN (mystorageacct.blob.core.windows.net) resolves to the private IP instead of the public one when queried from inside (or federated into) your network. This is handled with a Private DNS Zone (e.g., privatelink.blob.core.windows.net) linked to the VNet, populated automatically when you create the endpoint with DNS integration enabled. Skip this step and clients will resolve the public IP and either fail (if public access is locked down) or silently bypass the private path entirely — a configuration mistake that doesn't throw an error, it just quietly doesn't do what you assumed.

az network private-endpoint create \
  --name pe-storage-blob \
  --resource-group rg-network \
  --vnet-name vnet-spoke1 \
  --subnet snet-data \
  --private-connection-resource-id $STORAGE_ID \
  --group-id blob \
  --connection-name conn-storage-blob

For hub-and-spoke topologies, the standard pattern is a centralized Private DNS Zone linked to the hub VNet and shared to spokes via virtual network links, so every spoke resolves private endpoints consistently without each spoke owning its own DNS zone.

Putting it together: hybrid routing patterns

The pieces above compose into a small number of recurring topologies. A hub VNet holds the ExpressRoute or VPN gateway, a firewall/NVA, and shared Private DNS Zones. Spokes peer to the hub only, with use-remote-gateways enabled so they inherit on-prem reachability without their own gateway, and a UDR sending 0.0.0.0/0 (or specific ranges) to the hub firewall's private IP as next hop for anything that isn't VNet-local. On-prem routes to Azure over ExpressRoute (or VPN), the gateway advertises the hub and, via gateway transit, the spoke ranges too — but reaching a specific spoke's PaaS Private Endpoint from on-prem still depends on the on-prem DNS resolver being configured to forward queries for the relevant private-link zones to Azure DNS (via a DNS forwarder VM or Azure DNS Private Resolver in the hub), otherwise on-prem clients resolve the public IP instead.

Route propagation from ExpressRoute/VPN gateways into VNet route tables is automatic unless you explicitly disable BGP route propagation on a subnet's route table — a setting worth knowing about because a firewall or NVA subnet often disables it deliberately, forcing all egress through the appliance rather than letting the gateway's advertised routes create a shortcut around it. Getting this topology right is what actually decides whether your NSGs, peering, and gateway choices behave the way the design intended, rather than each layer quietly overriding the others.

Key Takeaways

  • Plan VNet address space for the peering mesh you'll eventually need, not just the VNet you're building today — overlapping ranges block peering entirely and are painful to renumber later
  • NSGs are stateful and evaluate the union of subnet-level and NIC-level rules — for inbound traffic the subnet NSG is checked first, for outbound the NIC NSG is checked first
  • Never rely on transitive routing through peered VNets; use a hub with gateway transit, UDRs pointing at an NVA/Azure Firewall, or Virtual WAN if you need many-to-many connectivity
  • ExpressRoute buys you SLA-backed bandwidth and predictable latency at a real cost and multi-week lead time; a VPN Gateway is provisioned in hours and is the right default until you have sustained hybrid traffic or compliance requirements that rule out the internet path
  • Private Endpoints give PaaS resources a NIC with a real private IP in your VNet — reachable from on-prem over ExpressRoute or VPN — which Service Endpoints never did

Related Articles

Azure AD & Entra ID: User ManagementAzure SQL Database vs SQL Server on VMsAzure VM Deployment, Sizing & PerformanceAzure Cost Management & FinOps