DeployU
Interviews / Cloud & DevOps / Design a hub-spoke VNet architecture with private endpoints and on-premises connectivity.

Design a hub-spoke VNet architecture with private endpoints and on-premises connectivity.

architecture Networking Interactive Quiz Code Examples

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.

Wrong Approach

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.

Right Approach

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

DecisionOption AOption BRecommendation
ConnectivityVNet PeeringVirtual WANPeering for under 50 VNets, vWAN for global
FirewallAzure FirewallNVAAzure Firewall for most cases
DNSAzure DNS PrivateCustom DNSAzure Private DNS Zones
GatewayPer-spokeShared in hubShared (cost efficient)

Private Endpoint DNS Zones

ServiceDNS Zone
Azure SQLprivatelink.database.windows.net
Storage Blobprivatelink.blob.core.windows.net
Key Vaultprivatelink.vaultcore.azure.net
Cosmos DBprivatelink.documents.azure.com
App Serviceprivatelink.azurewebsites.net

Practice Question

Why must you configure Private DNS Zones when using Private Endpoints?