A hand-built hub-and-spoke works beautifully until you have five of them on three continents, a mesh of VNet peerings nobody can draw from memory, and an NVA whose route tables are load-bearing tribal knowledge. Azure Virtual WAN is Microsoft’s answer: a managed global backbone where hubs are a resource type, transit is automatic, and the routing you maintained by hand becomes declarative. This is the build, end to end.
When hub-and-spoke stops scaling and Virtual WAN takes over
Classic hub-and-spoke is a pattern you assemble: a hub VNet, a VPN or ExpressRoute gateway, an NVA or Azure Firewall, and User-Defined Routes (UDRs) on every spoke forcing traffic through it. One region is fine; the pain compounds when you go global:
- No native hub-to-hub transit. Two regional hubs do not talk unless you peer them and hand-craft UDRs and gateway transit on both sides. Branch-to-branch across regions becomes a routing project.
- UDR sprawl. Every spoke needs route tables pointing at the firewall’s private IP. Multiply by spokes, regions, environments — drift is inevitable.
- Gateway and peering limits. Spoke and peering counts per VNet are bounded, and you feel it before you expect to.
Virtual WAN inverts the model. A Virtual Hub is a Microsoft-managed VNet containing a built-in router; you attach spokes, gateways, and firewalls as connections rather than wiring them. Hubs in different regions are joined by Microsoft’s backbone automatically, giving any-to-any transit — spoke-to-spoke, branch-to-branch, cross-region — without a single peering you maintain.
Use the Standard SKU. The Basic SKU only supports site-to-site VPN and gives you none of the transit, ExpressRoute, or routing features this article relies on. Basic exists for the smallest VPN-only cases; for an enterprise backbone it is a dead end.
Microsoft global backbone
___________________________|___________________________
| |
Virtual Hub: West Europe Virtual Hub: East US
(managed router + Azure FW) <== auto transit ==> (managed router + Azure FW)
| | | | | |
Spoke Spoke Branch (SD-WAN) Spoke Spoke ExpressRoute
VNets VNets + P2S users VNets VNets (on-prem DC)
The spokes, branches, and gateways stay yours. The hub, its router, and inter-hub transit are the platform’s problem now.
Step 1 - Virtual WAN topology: hubs, regions, and the global backbone
The Virtual WAN resource itself is just a logical container and a global setting; the hubs do the work. Provision one Virtual WAN, then a hub per region where you need a connectivity anchor.
LOCATION_EU=westeurope
LOCATION_US=eastus
RG=rg-vwan-global
VWAN=vwan-global
az group create --name $RG --location $LOCATION_EU
# Logical Virtual WAN container (Standard enables full transit + branch features)
az network vwan create \
--resource-group $RG \
--name $VWAN \
--location $LOCATION_EU \
--type Standard
# A managed hub per region. The /23 address space is internal to the hub
# and must NOT overlap any spoke, on-prem, or other hub range.
az network vhub create \
--resource-group $RG \
--vwan $VWAN \
--name hub-weu \
--location $LOCATION_EU \
--address-prefix 10.100.0.0/23 \
--sku Standard
az network vhub create \
--resource-group $RG \
--vwan $VWAN \
--name hub-eus \
--location $LOCATION_US \
--address-prefix 10.101.0.0/23 \
--sku Standard
Two address-planning rules will save you a rebuild:
- The hub prefix must be at least /24; /23 is the safe default. The hub packs a router, gateways, and firewall into that space, and you cannot resize it after creation. Undersize it and you cannot add a gateway later.
- Every prefix in the WAN must be globally unique — hubs, spokes, on-prem, branches. Virtual WAN advertises everything to everything by default; overlapping ranges produce black holes that are miserable to diagnose.
Hubs are joined the moment they exist under the same Virtual WAN. There is no peering step between hub-weu and hub-eus — that is the entire point.
Step 2 - Connecting spokes, branches (SD-WAN), and ExpressRoute
Spokes attach as hub virtual network connections. No peering, no gateway transit toggle, no UDRs to force traffic to the hub.
SPOKE_ID=$(az network vnet show -g rg-apps-weu -n vnet-app-weu --query id -o tsv)
az network vhub connection create \
--resource-group $RG \
--vhub-name hub-weu \
--name conn-app-weu \
--remote-vnet $SPOKE_ID
That connection alone gives the spoke transit to every other spoke and, across the backbone, to spokes behind hub-eus. Branch and ExpressRoute connectivity each need a gateway in the hub first. Gateways scale in scale units, not instance counts — one S2S VPN scale unit is roughly 500 Mbps aggregate, sized by total branch demand.
# Site-to-site VPN gateway in the hub (for IPsec branches / SD-WAN)
az network vpn-gateway create \
--resource-group $RG \
--name vpngw-weu \
--location $LOCATION_EU \
--vhub hub-weu \
--vpn-gateway-scale-unit 2
# ExpressRoute gateway in the hub (scale unit ~ 2 Gbps each)
az network express-route gateway create \
--resource-group $RG \
--name ergw-weu \
--location $LOCATION_EU \
--virtual-hub hub-weu \
--min-val 1 \
--max-val 2
For branches, model each physical site as a VPN Site (its public IP and on-prem prefixes, or BGP ASN) and connect it to the VPN gateway. At scale you do not click these one by one — Virtual WAN integrates with SD-WAN partners (Cisco, VMware, Barracuda, Fortinet and others) whose controllers provision the sites and IPsec tunnels into the hub automatically. The hub becomes the single managed concentrator instead of an NVA you patch.
ExpressRoute circuits connect to the ExpressRoute gateway and immediately gain transit to every spoke and, via the backbone, to remote regions — a single circuit in Europe can reach US workloads. Branch and ExpressRoute prefixes propagate into the hub over BGP; there is no UDR step.
Step 3 - Secured virtual hubs with Azure Firewall and routing intent
A hub with a firewall in it is a secured virtual hub. Historically you secured one by writing custom route tables that pointed 0.0.0.0/0 and the RFC1918 ranges at the firewall. That still works, but routing intent is the modern, far cleaner mechanism: you declare intent and the platform programs every route table in the hub for you.
First, deploy Azure Firewall into the hub via a firewall policy.
# Firewall policy (define rules, threat intel, premium features here)
az network firewall policy create \
--resource-group $RG \
--name fwpol-global \
--location $LOCATION_EU
# Azure Firewall anchored to the hub
az network firewall create \
--resource-group $RG \
--name azfw-weu \
--location $LOCATION_EU \
--vhub hub-weu \
--firewall-policy fwpol-global \
--sku AZFW_Hub \
--tier Standard \
--count 1
Then turn on routing intent. Two policy types exist: InternetTraffic (0.0.0.0/0) and PrivateTraffic (RFC1918 plus any custom private prefixes). Setting both to route through the firewall means every flow the hub sees — spoke-to-spoke, spoke-to-internet, branch-to-spoke, and cross-hub — is inspected, with no per-spoke UDRs anywhere.
{
"routingPolicies": [
{
"name": "InternetTrafficPolicy",
"destinations": ["Internet"],
"nextHop": "<azure-firewall-resource-id>"
},
{
"name": "PrivateTrafficPolicy",
"destinations": ["PrivateTraffic"],
"nextHop": "<azure-firewall-resource-id>"
}
]
}
Routing intent is configured per hub — enable it on every secured hub. Once both policies route to the firewall, inter-hub traffic is inspected by the firewall in each hub it transits, so plan firewall capacity for east-west volume, not just internet egress.
This is the single biggest operational win of Virtual WAN: the security posture that used to be hundreds of brittle UDRs becomes two declarative policies the platform keeps consistent.
Step 4 - Custom route tables and controlling any-to-any traffic flow
Any-to-any is the default, and for many estates that is exactly wrong. You usually want isolation — production cannot reach development; a shared-services spoke (DNS, AD, management) is reachable by everyone but cannot freely initiate into the rest.
The control plane is hub route tables plus labels. Each connection has two properties:
- Associated route table - the single table the connection consults to decide where its outbound traffic goes.
- Propagated route tables / labels - which tables learn this connection’s prefixes (i.e., who can reach it).
Every hub ships a Default and a None route table. Add your own for segmentation.
# A route table that production spokes associate with
az network vhub route-table create \
--resource-group $RG \
--vhub-name hub-weu \
--name rt-prod \
--labels prod
az network vhub route-table create \
--resource-group $RG \
--vhub-name hub-weu \
--name rt-dev \
--labels dev
To isolate prod from dev while both reach shared services: associate prod spokes with rt-prod propagating to prod and shared; associate dev spokes with rt-dev propagating to dev and shared; have the shared-services spoke propagate to all three. Because prod’s table never learns dev’s prefixes, segmentation is enforced in the route tables themselves — not in a firewall rule you might fat-finger.
| Connection | Associated table | Propagates to labels | Reachable from |
|---|---|---|---|
| Prod spokes | rt-prod | prod, shared | Prod, shared |
| Dev spokes | rt-dev | dev, shared | Dev, shared |
| Shared services | Default | prod, dev, shared | Everyone |
| ExpressRoute / branches | Default | prod, dev, shared | Everyone (tune as needed) |
When routing intent is enabled, the platform injects the firewall next hop into these tables for you, so segmentation by route table and inspection by the firewall compose cleanly. Use static routes in a route table only for the exceptions — e.g. forcing a specific prefix to a third-party NVA the firewall does not front.
Step 5 - Integrating point-to-site and remote-user access at scale
Remote users land on the same backbone as everything else through a point-to-site (P2S) VPN gateway in the hub. Once connected, a user reaches any spoke or on-prem prefix the hub knows — global reach from a single client profile.
# P2S gateway sized by concurrent users; scale unit ~500 connections
az network p2s-vpn-gateway create \
--resource-group $RG \
--name p2sgw-weu \
--location $LOCATION_EU \
--vhub hub-weu \
--scale-unit 2 \
--vpn-server-config <vpn-server-config-id> \
--address-space 10.200.0.0/24
Two things matter at enterprise scale:
- Authenticate against Entra ID (OpenVPN protocol), not certificates. A VPN server configuration with Entra ID auth gives you Conditional Access and MFA on the tunnel and kills certificate distribution. RADIUS and certificate auth exist but do not scale as cleanly for a global workforce.
- The P2S client address pool is just another prefix in the WAN — keep it globally unique and size it for peak concurrency. Users on the EU hub reach US spokes over the backbone, so you rarely need a gateway in every hub; place P2S gateways where your users physically are to minimize first-hop latency.
With routing intent on, remote-user traffic to private prefixes (and internet, if force-tunneled) is inspected by the hub firewall like any other flow — same policy, no special cases.
Step 6 - Observability, routing diagnostics, and cost control
The managed router is convenient until you need to know why a packet went somewhere. The tools that help:
- Effective routes. First stop for any reachability problem — it shows the routes the hub actually programmed, including intent-injected firewall next hops.
az network vhub get-effective-routes \
--resource-group $RG \
--name hub-weu \
--resource-type RouteTable \
--resource-id <route-table-resource-id>
- VPN gateway BGP peer status to confirm branches and on-prem are exchanging the prefixes you expect.
az network vpn-gateway list-bgp-peer-status \
--resource-group $RG \
--name vpngw-weu
- Diagnostic settings to Log Analytics on hub gateways and the firewall, so tunnel events, route changes, and firewall verdicts are queryable in KQL rather than guessed at.
- VNet flow logs on spokes and Connection Monitor for synthetic hub-to-hub and branch-to-spoke probes.
On cost, Virtual WAN bills on axes hub-and-spoke does not:
| Cost driver | What to watch |
|---|---|
| Hub deployment + routing infra | Per-hour charge per hub — consolidate regions; do not spin a hub per environment |
| Connection units | Per-hour charge scaling with the number of connected VNets/branches |
| Data processed by the hub router | Per-GB on traffic the hub routes — east-west across hubs adds up |
| Gateway scale units | VPN/ER/P2S billed per scale unit whether or not saturated |
| Azure Firewall in hub | Standard firewall pricing applies on top of the hub |
The data-processing charge is the one that bites: chatty cross-region spoke-to-spoke traffic that was “free” intra-region peering is now metered per GB. Keep high-volume pairs in the same region’s hub, and reserve cross-hub transit for genuinely global flows.
Verify
Confirm the backbone behaves before you migrate production onto it.
# 1. Both hubs exist and are provisioned under one Virtual WAN
az network vhub list --resource-group $RG \
--query "[].{name:name, state:provisioningState, addr:addressPrefix}" -o table
# 2. Routing intent is active on the secured hubs
az network vhub routing-intent list \
--resource-group $RG --vhub-name hub-weu -o table
# 3. The firewall in the hub is healthy
az network firewall show \
--resource-group $RG --name azfw-weu \
--query "{name:name, state:provisioningState}" -o table
# 4. A spoke connection sees the firewall next hop and remote-region prefixes
az network vhub get-effective-routes \
--resource-group $RG --name hub-weu \
--resource-type RouteTable --resource-id <route-table-resource-id>
End-to-end functional checks:
- From a prod spoke VM, reach a prod spoke behind the other hub (cross-region transit works).
- From a prod spoke, confirm you cannot reach a dev spoke (segmentation holds).
- Connect a P2S client and reach a spoke in a remote region (global remote access works).
- Inspect Azure Firewall logs in Log Analytics and confirm the above flows appear as inspected (routing intent is actually in path).
Migration checklist: hub-spoke to Virtual WAN without downtime
The move is incremental — run both topologies side by side and cut spokes over one at a time.
Enterprise scenario
A retail group ran two secured hubs (hub-weu, hub-eus) with routing intent on both. Months after go-live, a new product team in West Europe complained their spoke could not reach a SaaS-style internal API hosted on a spoke behind East US. Connectivity to other EU spokes was fine, so it looked like a cross-region transit failure.
It was not. get-effective-routes on the EU spoke showed the East US prefix learned correctly, next-hop the local firewall — routing was healthy. The drops were in the East US Azure Firewall: when both InternetTraffic and PrivateTraffic route through the firewall, cross-hub east-west traffic is inspected in the destination hub too, and that hub’s policy had no rule permitting the EU spoke range.
The fix was a network rule in the shared parent policy (so both hubs inherit it), scoped to the actual prefixes rather than a blanket allow:
az network firewall policy rule-collection-group collection add-filter-collection \
--resource-group $RG \
--policy-name fwpol-global \
--rule-collection-group-name rcg-eastwest \
--name allow-weu-to-eus-api \
--collection-priority 200 \
--action Allow \
--rule-name weu-spokes-to-api \
--rule-type NetworkRule \
--source-addresses 10.20.0.0/16 \
--destination-addresses 10.30.4.0/24 \
--destination-ports 443 \
--ip-protocols TCP
The lesson the team wrote into their runbook: with routing intent, a packet crossing two hubs is inspected by two firewalls. Effective routes proving reachability is only half the diagnosis — always check the firewall logs in the destination hub before suspecting the backbone.
Pitfalls and next steps
The mistakes that cause re-architecture, not just tickets:
- Choosing Basic SKU. It cannot be upgraded to the feature set you need. Start Standard.
- Undersizing the hub prefix. The address space is immutable; too small forces a full hub rebuild to add a gateway.
- Forgetting routing intent is per hub. One secured hub and one wide-open hub on the same WAN is a silent security gap.
- Assuming any-to-any is desirable. Default transit means dev can reach prod until you say otherwise. Design route tables and labels before you attach production spokes.
- Ignoring the data-processing meter. Cross-hub east-west traffic is billed per GB; “just route it through the backbone” has a line item.
From here, codify the topology in Terraform or Bicep so hubs, route tables, labels, and routing intent are reviewed and reproducible rather than portal artifacts; structure firewall policy into a parent/child hierarchy for per-region overrides; and fold the Virtual WAN into your Azure Landing Zone connectivity subscription so it becomes the sanctioned global network, not a one-off.