Questions
Design a hub-spoke VNet architecture with private endpoints and on-premises connectivity.
The Scenario
Your enterprise needs to migrate workloads to Azure while:
- Maintaining connectivity to on-premises data centers
- Keeping all traffic private (no public endpoints)
- Isolating workloads across multiple subscriptions
- Enabling shared services (DNS, firewalls, monitoring)
- Meeting compliance requirements for data residency
Current state: Each team has their own VNet with no connectivity, using public endpoints.
The Challenge
Design a hub-spoke network architecture using Azure networking services: VNet peering, Private Link, ExpressRoute, and Azure Firewall.
A junior engineer might create point-to-point VPN connections between all VNets (mesh), use public endpoints with IP restrictions, skip centralized DNS, or deploy firewalls in each spoke. These approaches don't scale, expose attack surface, break private endpoint resolution, and waste resources.
A senior engineer designs a hub-spoke topology with centralized connectivity (ExpressRoute/VPN), Azure Firewall for traffic inspection, Private DNS zones for private endpoint resolution, and uses Azure Virtual WAN for global scale.
Architecture Overview
On-Premises Data Center
│
│ ExpressRoute / VPN
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hub VNet (10.0.0.0/16) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Gateway Subnet │ │ Firewall Subnet│ │ Bastion Subnet │ │
│ │ 10.0.0.0/24 │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │
│ └────────┬────────┘ └────────┬────────┘ └─────────────────┘ │
│ │ │ │
│ │ ┌────────┴────────┐ │
│ │ │ Azure Firewall │ │
│ │ └────────┬────────┘ │
└───────────┼────────────────────┼────────────────────────────────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Peering │ │ Peering │
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Spoke 1 │ │ Spoke 2 │ │ Spoke 3 │ │ Spoke 4 │
│ Prod │ │ Dev │ │ Shared │ │ DMZ │
│10.1.0/16 │ │10.2.0/16 │ │10.3.0/16 │ │10.4.0/16 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘Step 1: Create Hub VNet with Subnets
// hub-vnet.bicep
param location string = resourceGroup().location
resource hubVnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: 'hub-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
subnets: [
{
name: 'GatewaySubnet' // Must be named exactly this
properties: {
addressPrefix: '10.0.0.0/24'
}
}
{
name: 'AzureFirewallSubnet' // Must be named exactly this
properties: {
addressPrefix: '10.0.1.0/24'
}
}
{
name: 'AzureBastionSubnet' // Must be named exactly this
properties: {
addressPrefix: '10.0.2.0/24'
}
}
{
name: 'SharedServicesSubnet'
properties: {
addressPrefix: '10.0.3.0/24'
}
}
]
}
}
// Azure Firewall
resource firewall 'Microsoft.Network/azureFirewalls@2023-05-01' = {
name: 'hub-firewall'
location: location
properties: {
sku: {
name: 'AZFW_VNet'
tier: 'Standard'
}
ipConfigurations: [
{
name: 'fw-ipconfig'
properties: {
subnet: {
id: '${hubVnet.id}/subnets/AzureFirewallSubnet'
}
publicIPAddress: {
id: firewallPublicIP.id
}
}
}
]
firewallPolicy: {
id: firewallPolicy.id
}
}
}
// Firewall Policy
resource firewallPolicy 'Microsoft.Network/firewallPolicies@2023-05-01' = {
name: 'hub-firewall-policy'
location: location
properties: {
sku: {
tier: 'Standard'
}
threatIntelMode: 'Alert'
dnsSettings: {
enableProxy: true // Required for FQDN rules
}
}
}Step 2: Create Spoke VNets with Peering
// spoke-vnet.bicep
param spokeName string
param spokeAddressPrefix string
param hubVnetId string
param firewallPrivateIP string
resource spokeVnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: '${spokeName}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [spokeAddressPrefix]
}
subnets: [
{
name: 'default'
properties: {
addressPrefix: cidrSubnet(spokeAddressPrefix, 24, 0)
routeTable: {
id: spokeRouteTable.id
}
networkSecurityGroup: {
id: spokeNSG.id
}
}
}
{
name: 'PrivateEndpoints'
properties: {
addressPrefix: cidrSubnet(spokeAddressPrefix, 24, 1)
privateEndpointNetworkPolicies: 'Disabled'
}
}
]
}
}
// Peering: Spoke to Hub
resource spokeToHubPeering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2023-05-01' = {
parent: spokeVnet
name: 'spoke-to-hub'
properties: {
remoteVirtualNetwork: {
id: hubVnetId
}
allowVirtualNetworkAccess: true
allowForwardedTraffic: true
useRemoteGateways: true // Use hub's gateway
}
}
// Route table to force traffic through firewall
resource spokeRouteTable 'Microsoft.Network/routeTables@2023-05-01' = {
name: '${spokeName}-rt'
location: location
properties: {
routes: [
{
name: 'to-internet'
properties: {
addressPrefix: '0.0.0.0/0'
nextHopType: 'VirtualAppliance'
nextHopIpAddress: firewallPrivateIP
}
}
{
name: 'to-onprem'
properties: {
addressPrefix: '192.168.0.0/16' // On-prem range
nextHopType: 'VirtualAppliance'
nextHopIpAddress: firewallPrivateIP
}
}
]
}
}Step 3: Configure ExpressRoute for On-Premises
// ExpressRoute Gateway
resource expressRouteGateway 'Microsoft.Network/virtualNetworkGateways@2023-05-01' = {
name: 'hub-expressroute-gw'
location: location
properties: {
gatewayType: 'ExpressRoute'
sku: {
name: 'ErGw2AZ' // Zone-redundant
tier: 'ErGw2AZ'
}
ipConfigurations: [
{
name: 'gwipconfig'
properties: {
subnet: {
id: '${hubVnet.id}/subnets/GatewaySubnet'
}
publicIPAddress: {
id: gatewayPublicIP.id
}
}
}
]
}
}
// ExpressRoute Connection
resource expressRouteConnection 'Microsoft.Network/connections@2023-05-01' = {
name: 'hub-to-onprem'
location: location
properties: {
connectionType: 'ExpressRoute'
virtualNetworkGateway1: {
id: expressRouteGateway.id
}
peer: {
id: expressRouteCircuit.id
}
routingWeight: 0
}
}Step 4: Private Endpoints and DNS
// Private endpoint for Azure SQL
resource sqlPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
name: 'sql-private-endpoint'
location: location
properties: {
subnet: {
id: '${spokeVnet.id}/subnets/PrivateEndpoints'
}
privateLinkServiceConnections: [
{
name: 'sql-connection'
properties: {
privateLinkServiceId: sqlServer.id
groupIds: ['sqlServer']
}
}
]
}
}
// Private DNS Zone
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.database.windows.net'
location: 'global'
}
// Link DNS zone to VNets
resource hubDnsLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
parent: privateDnsZone
name: 'hub-link'
location: 'global'
properties: {
virtualNetwork: {
id: hubVnet.id
}
registrationEnabled: false
}
}
// DNS A record for private endpoint
resource sqlDnsRecord 'Microsoft.Network/privateDnsZones/A@2020-06-01' = {
parent: privateDnsZone
name: sqlServer.name
properties: {
ttl: 300
aRecords: [
{
ipv4Address: sqlPrivateEndpoint.properties.customDnsConfigs[0].ipAddresses[0]
}
]
}
}Step 5: Azure Firewall Rules
resource firewallRuleCollection 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2023-05-01' = {
parent: firewallPolicy
name: 'DefaultRuleCollectionGroup'
properties: {
priority: 100
ruleCollections: [
{
ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
name: 'AllowAzureServices'
priority: 100
action: {
type: 'Allow'
}
rules: [
{
ruleType: 'ApplicationRule'
name: 'AllowAzureMonitor'
protocols: [
{ protocolType: 'Https', port: 443 }
]
targetFqdns: [
'*.monitor.azure.com'
'*.ods.opinsights.azure.com'
'*.oms.opinsights.azure.com'
]
sourceAddresses: ['10.0.0.0/8']
}
]
}
{
ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
name: 'AllowInternet'
priority: 200
action: {
type: 'Allow'
}
rules: [
{
ruleType: 'ApplicationRule'
name: 'AllowWindowsUpdate'
protocols: [
{ protocolType: 'Https', port: 443 }
]
targetFqdns: [
'*.windowsupdate.com'
'*.microsoft.com'
]
sourceAddresses: ['10.0.0.0/8']
}
]
}
]
}
}Step 6: Network Security Groups
resource spokeNSG 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: '${spokeName}-nsg'
location: location
properties: {
securityRules: [
{
name: 'AllowHTTPSInbound'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: 'VirtualNetwork'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
}
}
{
name: 'DenyAllInbound'
properties: {
priority: 4096
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
]
}
} Hub-Spoke Design Decisions
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Connectivity | VNet Peering | Virtual WAN | Peering for under 50 VNets, vWAN for global |
| Firewall | Azure Firewall | NVA | Azure Firewall for most cases |
| DNS | Azure DNS Private | Custom DNS | Azure Private DNS Zones |
| Gateway | Per-spoke | Shared in hub | Shared (cost efficient) |
Private Endpoint DNS Zones
| Service | DNS Zone |
|---|---|
| Azure SQL | privatelink.database.windows.net |
| Storage Blob | privatelink.blob.core.windows.net |
| Key Vault | privatelink.vaultcore.azure.net |
| Cosmos DB | privatelink.documents.azure.com |
| App Service | privatelink.azurewebsites.net |
Practice Question
Why must you configure Private DNS Zones when using Private Endpoints?