Benefits of a Private ARO Cluster in Azure

Private ARO Cluster

A private ARO cluster removes all public IP addresses from both the Kubernetes API server and the ingress router — making the cluster completely unreachable from the internet. Every connection to the cluster must travel over Azure’s private network backbone via VNet peering, ExpressRoute, or VPN.


Public vs Private ARO — What Changes

ComponentPublic clusterPrivate cluster
API server endpointPublic IP + DNSPrivate endpoint IP only
Ingress routerPublic load balancerInternal load balancer
Worker node IPsPrivate (always)Private (always)
Master node IPsPrivate (always)Private (always)
Access methodAny internet browserVPN / ER / Bastion only
DNS resolutionPublic DNSPrivate DNS zone
Attack surfaceAPI port 6443 exposedZero public exposure

How the API Server Is Hidden — The Mechanism

When you deploy a private ARO cluster, Azure does three things automatically:

1. API Server gets a Private Endpoint NIC

Instead of a public load balancer frontend, the API server is exposed exclusively through a private endpoint — a NIC in your VNet subnet with a private IP:

Public cluster:
  api.cluster.eastus.aroapp.io → 20.x.x.x (public IP)
  Anyone on internet can reach :6443

Private cluster:
  api.cluster.eastus.aroapp.io → 10.1.3.4 (private IP in your VNet)
  Only reachable from within the VNet or peered networks

The private endpoint is deployed into your ARO master subnet automatically during cluster creation. No public IP is allocated.

2. A Private DNS Zone Is Created Automatically

ARO creates a private DNS zone linked to your VNet so the API server FQDN resolves to the private endpoint IP:

Private DNS zone: cluster.eastus.aroapp.io
  A record: api → 10.1.3.4
  A record: *.apps → 10.1.4.8   (ingress internal LB)

Linked to: ARO spoke VNet + hub VNet

This means any VM in a peered VNet can resolve api.cluster.eastus.aroapp.io and get 10.1.3.4 — no public DNS lookup ever occurs.

3. Ingress Router Gets an Internal Load Balancer

The OpenShift ingress router (which handles *.apps.cluster.aroapp.io routes) is fronted by an Azure Internal Load Balancer with a private frontend IP:

Public cluster:   *.apps → Azure Public LB → 20.x.x.x
Private cluster:  *.apps → Azure Internal LB → 10.1.4.8

Applications running on the cluster are only reachable from inside the VNet or connected networks.


Deploying a Private ARO Cluster

# 1. Create resource group and VNet
az group create --name rg-aro --location eastus

az network vnet create \
  --resource-group rg-aro \
  --name aro-spoke-vnet \
  --address-prefixes 10.1.0.0/16

# 2. Create master subnet — disable private endpoint network policies
az network vnet subnet create \
  --resource-group rg-aro \
  --vnet-name aro-spoke-vnet \
  --name master-subnet \
  --address-prefixes 10.1.0.0/24 \
  --disable-private-link-service-network-policies true  # ← required for ARO

# 3. Create worker subnet
az network vnet subnet create \
  --resource-group rg-aro \
  --vnet-name aro-spoke-vnet \
  --name worker-subnet \
  --address-prefixes 10.1.1.0/23

# 4. Deploy private ARO cluster
az aro create \
  --resource-group rg-aro \
  --name aro-prod \
  --vnet aro-spoke-vnet \
  --master-subnet master-subnet \
  --worker-subnet worker-subnet \
  --apiserver-visibility Private \     # ← API server private
  --ingress-visibility Private \       # ← ingress private
  --master-vm-size Standard_D8s_v3 \
  --worker-vm-size Standard_D16s_v3 \
  --worker-count 3 \
  --pull-secret @pull-secret.txt

# Takes ~35 minutes to complete


The Three Access Paths

Path 1 — Azure Bastion + Jump Host (most common)

The simplest pattern — a small Linux VM in the hub VNet with oc and kubectl installed, accessed securely via Bastion:

# 1. Admin opens Azure portal → connects via Bastion to jump-host-vm
# 2. On jump host — get cluster credentials
az aro list-credentials \
  --resource-group rg-aro \
  --name aro-prod

# Output:
# kubeadminPassword: "XXXXX-XXXXX-XXXXX-XXXXX"
# kubeadminUsername: "kubeadmin"

# 3. Get API server URL
API_URL=$(az aro show \
  --resource-group rg-aro \
  --name aro-prod \
  --query apiserverProfile.url -o tsv)

# 4. Login — works because jump host is in peered VNet
oc login $API_URL \
  --username kubeadmin \
  --password XXXXX-XXXXX-XXXXX-XXXXX

# 5. Verify
oc get nodes
oc get clusterversion


Path 2 — ExpressRoute / VPN from on-premises

On-premises developers access the private API server directly from their workstations — but DNS must be configured to resolve the ARO private DNS zone:

On-premises developer workstation
Corporate DNS server: api.cluster.eastus.aroapp.io
↓ conditional forward to Azure DNS Private Resolver (10.0.5.4)
Azure DNS Private Resolver
↓ linked private DNS zone: aroapp.io → 10.1.3.4
Returns: 10.1.3.4
Developer runs: oc login https://api.cluster.eastus.aroapp.io:6443
Traffic travels: workstation → MPLS → ER Gateway → hub VNet peering → ARO spoke → API server

On-premises DNS server conditional forwarder:

Zone: aroapp.io
Forward to: 10.0.5.4 (DNS Private Resolver inbound endpoint)

Path 3 — CI/CD Pipeline (GitHub Actions / Azure DevOps)

For automated deployments, pipelines must also reach the private API server. Use a self-hosted runner inside the VNet:

# GitHub Actions — self-hosted runner in hub VNet
name: Deploy to ARO
on: [push]
jobs:
  deploy:
    runs-on: self-hosted    # ← runner VM inside Azure VNet
    steps:
      - uses: actions/checkout@v4

      - name: Login to ARO
        run: |
          oc login ${{ secrets.ARO_API_URL }} \
            --token ${{ secrets.ARO_SERVICE_ACCOUNT_TOKEN }}

      - name: Deploy application
        run: |
          oc apply -f k8s/
          oc rollout status deployment/my-app

The self-hosted runner is a VM in the hub VNet — it can resolve the private API server DNS and reach port 6443 over VNet peering.


Private DNS — The Critical Detail

After cluster creation, ARO automatically creates a private DNS zone. You must link this zone to every VNet that needs to resolve the API server — including the hub VNet where your jump host and DNS Private Resolver live:

# ARO creates this automatically — linked to ARO spoke VNet
# You must manually link it to the hub VNet

PRIVATE_ZONE=$(az network private-dns zone list \
  --resource-group $(az aro show -g rg-aro -n aro-prod \
    --query clusterProfile.resourceGroupId -o tsv | tr -d '\n') \
  --query "[?contains(name,'aroapp.io')].name" -o tsv)

# Link to hub VNet
az network private-dns link vnet create \
  --resource-group <aro-managed-rg> \
  --zone-name $PRIVATE_ZONE \
  --name link-to-hub-vnet \
  --virtual-network $(az network vnet show \
    --resource-group rg-hub \
    --name hub-vnet --query id -o tsv) \
  --registration-enabled false

Without this link, VMs in the hub VNet cannot resolve api.cluster.eastus.aroapp.io — DNS queries fall through to public DNS which returns NXDOMAIN for a private cluster.


Entra ID (AAD) Integration for Developer Access

Replace the kubeadmin local account with Entra ID authentication — developers log in with their corporate credentials:

# Configure AAD OAuth on ARO
az aro update \
  --resource-group rg-aro \
  --name aro-prod \
  --client-id <app-registration-client-id> \
  --client-secret <app-registration-secret>


# Grant cluster-admin to an AAD group
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: aro-cluster-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: Group
    apiGroup: rbac.authorization.k8s.io
    name: <aad-group-object-id>    # e.g. Platform Engineering team
---
# Grant view-only to a developer group
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dev-view
  namespace: my-app
roleRef:
  kind: ClusterRole
  name: view
subjects:
  - kind: Group
    name: <dev-aad-group-object-id>


Developers now login via:

oc login $API_URL # redirects to Microsoft login page
# Enter corporate credentials → MFA → issued a token

Egress from a Private Cluster

A private cluster still needs outbound internet access for Red Hat image registries and update servers. Force all egress through Azure Firewall via UDR on both subnets:

# Route table for master and worker subnets
az network route-table create \
  --resource-group rg-aro-network \
  --name rt-aro-subnets

az network route-table route create \
  --resource-group rg-aro-network \
  --route-table-name rt-aro-subnets \
  --name force-to-firewall \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address 10.0.1.4     # Azure Firewall private IP

# Associate with master subnet
az network vnet subnet update \
  --resource-group rg-aro \
  --vnet-name aro-spoke-vnet \
  --name master-subnet \
  --route-table rt-aro-subnets

# Associate with worker subnet
az network vnet subnet update \
  --resource-group rg-aro \
  --vnet-name aro-spoke-vnet \
  --name worker-subnet \
  --route-table rt-aro-subnets

Required Azure Firewall FQDN allow rules for private ARO:

quay.io                          # Red Hat image registry
registry.redhat.io               # Red Hat registry
registry.access.redhat.com       # RHEL content
cdn.quay.io                      # CDN for quay
*.blob.core.windows.net          # Azure storage (etcd backups, images)
*.servicebus.windows.net         # ARO monitoring
management.azure.com             # Azure ARM API
login.microsoftonline.com        # Entra ID auth


Key Takeaway

A private ARO cluster achieves zero public attack surface by replacing the public API server load balancer with a VNet-internal private endpoint, and replacing the public ingress load balancer with an internal one. DNS resolution of both endpoints stays entirely within Azure’s private network. The only access paths are Azure Bastion for interactive access, ExpressRoute or VPN for on-premises connectivity, and self-hosted CI/CD runners for automation — all travelling over encrypted private paths without a single packet touching the public internet.

Best Practices for OpenShift on Azure: ARO Guide

OpenShift Container Platform on Azure — ARO Best Practices

Azure Red Hat OpenShift (ARO) is a fully managed OpenShift 4 service jointly operated by Microsoft and Red Hat — both companies share responsibility for the control plane, infrastructure, and SLA (99.95%).


1. Networking Best Practices

Always deploy a private cluster

A private ARO cluster hides the Kubernetes API server behind a private endpoint — no public IP, unreachable from the internet:

az aro create \
  --resource-group rg-aro \
  --name aro-prod \
  --vnet aro-spoke-vnet \
  --master-subnet master-subnet \
  --worker-subnet worker-subnet \
  --apiserver-visibility Private \      # ← API server private
  --ingress-visibility Private \        # ← ingress private
  --pull-secret @pull-secret.txt


Access to the private API server is then through Azure Bastion → jump host, or over ExpressRoute/VPN from on-premises.


Subnet sizing — get this right before deployment (cannot resize after)

ARO consumes IP addresses aggressively — every pod gets its own IP from the node’s subnet range:

SubnetMinimumRecommendedNotes
Master subnet/27/24Fixed 3 masters — needs room for Azure infra IPs
Worker subnet/27/23 or /22Every pod consumes an IP — size generously
Ingress subnet/28/27For LB / App Gateway front-end IPs
Private endpoints/28/27One IP per private endpoint
Worker subnet sizing example:
  /23 = 512 addresses
  Azure reserves 5
  Available: 507
  Max pods per node: 250 (default OpenShift SDN)
  Nodes supportable: ~2 per node × workers
  Plan for: 3× current need for growth headroom



Egress lockdown via Azure Firewall

ARO requires outbound internet access for Red Hat update servers, telemetry, and pull.registry.redhat.io. Lock this down with Azure Firewall application rules rather than allowing all outbound:

Azure Firewall Application Rules for ARO egress:
┌─────────────────────────────────────────────────────────┐
│ Name Target FQDN │
├─────────────────────────────────────────────────────────┤
│ aro-rh-registry registry.redhat.io │
│ registry.access.redhat.com │
│ quay.io │
│ cdn.quay.io │
├─────────────────────────────────────────────────────────┤
│ aro-azure-services *.blob.core.windows.net │
│ *.servicebus.windows.net │
│ *.table.core.windows.net │
├─────────────────────────────────────────────────────────┤
│ aro-monitoring *.ods.opinsights.azure.com │
│ *.oms.opinsights.azure.com │
├─────────────────────────────────────────────────────────┤
│ aro-rh-telemetry cert-api.access.redhat.com │
│ api.access.redhat.com │
└─────────────────────────────────────────────────────────┘

Apply a UDR on the master and worker subnets pointing 0.0.0.0/0 to the Azure Firewall private IP — same hub and spoke pattern as any spoke workload.


Use a custom DNS server

Point the ARO VNet DNS to your hub DNS Private Resolver so cluster nodes can resolve private endpoints and internal domains:

az network vnet update \
  --resource-group rg-aro-network \
  --name aro-spoke-vnet \
  --dns-servers 10.0.5.4    # DNS Private Resolver inbound endpoint IP



2. Availability and Resilience Best Practices

Spread across all three Availability Zones

ARO deploys 3 master nodes — one per AZ automatically. Workers must be explicitly spread via MachineSets:

# MachineSet for AZ1 — replicate for AZ2, AZ3
apiVersion: machine.openshift.io/v1beta1
kind: MachineSet
metadata:
  name: aro-prod-worker-eastus-1
  namespace: openshift-machine-api
spec:
  replicas: 3
  template:
    spec:
      providerSpec:
        value:
          zone: "1"                         # AZ1
          vmSize: Standard_D16s_v3
          osDisk:
            diskSizeGB: 128
            managedDisk:
              storageAccountType: Premium_LRS


Create three MachineSets — one per zone — with equal replica counts. This ensures workloads survive a full AZ failure.


Enable cluster autoscaler

apiVersion: autoscaling.openshift.io/v1
kind: ClusterAutoscaler
metadata:
  name: default
spec:
  resourceLimits:
    maxNodesTotal: 24
  scaleDown:
    enabled: true
    delayAfterAdd: 10m
    delayAfterDelete: 5m
    delayAfterFailure: 30s
---
apiVersion: autoscaling.openshift.io/v1beta1
kind: MachineAutoscaler
metadata:
  name: aro-prod-worker-eastus-1
  namespace: openshift-machine-api
spec:
  minReplicas: 3
  maxReplicas: 8
  scaleTargetRef:
    kind: MachineSet
    name: aro-prod-worker-eastus-1



Use zone-redundant storage for persistent volumes

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-premium-zrs
provisioner: disk.csi.azure.com
parameters:
  skuName: Premium_ZRS       # Zone-redundant storage — survives AZ failure
  cachingMode: ReadOnly
reclaimPolicy: Retain        # Retain on PVC delete — prevents data loss
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true


Use Premium_ZRS instead of Premium_LRS for stateful workloads — ZRS replicates the disk synchronously across three AZs so a pod can reschedule to another zone without losing its data.


3. Security Best Practices

Use Workload Identity (pod-level Azure RBAC)

Never put Azure credentials in pods. Use Workload Identity to give individual pods an Azure AD identity with scoped RBAC permissions:

# Enable workload identity on ARO cluster
az aro update \
  --resource-group rg-aro \
  --name aro-prod \
  --enable-managed-identity

# Create a managed identity for a specific workload
az identity create \
  --resource-group rg-aro-workloads \
  --name id-payment-service

# Grant it only what it needs
az role assignment create \
  --assignee <identity-client-id> \
  --role "Key Vault Secrets User" \
  --scope /subscriptions/.../vaults/kv-prod


# Annotate the service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-service-sa
  namespace: payments
  annotations:
    azure.workload.identity/client-id: "<managed-identity-client-id>"



Integrate Azure Key Vault for secrets via CSI driver

Never store secrets in OpenShift Secrets (base64 is not encryption). Use the Secrets Store CSI driver to mount Key Vault secrets directly into pods:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-kv-secrets
  namespace: payments
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    clientID: "<managed-identity-client-id>"
    keyvaultName: kv-prod
    tenantID: "<tenant-id>"
    objects: |
      array:
        - |
          objectName: db-connection-string
          objectType: secret
        - |
          objectName: api-key
          objectType: secret



Integrate with Azure Container Registry via private endpoint

# Create ACR with private endpoint — no public access
az acr create \
  --resource-group rg-aro \
  --name acrprodaro \
  --sku Premium \
  --public-network-enabled false

# Private endpoint in ARO spoke
az network private-endpoint create \
  --name pe-acr-prod \
  --resource-group rg-aro-network \
  --vnet-name aro-spoke-vnet \
  --subnet private-endpoint-subnet \
  --private-connection-resource-id $(az acr show --name acrprodaro --query id -o tsv) \
  --group-id registry \
  --connection-name pe-acr-conn

# Grant ARO pull access
az role assignment create \
  --assignee <aro-kubelet-identity> \
  --role AcrPull \
  --scope $(az acr show --name acrprodaro --query id -o tsv)



Apply OpenShift Security Context Constraints (SCC)

Never run pods as root. Use the restricted-v2 SCC (default in OpenShift 4.11+):

apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
        readOnlyRootFilesystem: true



Enable Microsoft Defender for Containers

az security pricing create \
  --name Containers \
  --tier Standard


Defender for Containers provides runtime threat detection, vulnerability scanning for images in ACR, and Kubernetes audit log analysis — all surfaced in Microsoft Defender for Cloud.


4. Observability Best Practices

Forward logs to Azure Monitor / Log Analytics

# Enable container insights on ARO
az aro update \
  --resource-group rg-aro \
  --name aro-prod \
  --enable-managed-identity

# Deploy the monitoring add-on via Helm
helm repo add microsoft https://microsoft.github.io/charts/repo
helm install azuremonitor-containers \
  microsoft/azuremonitor-containers \
  --set omsagent.secret.wsid=<workspace-id> \
  --set omsagent.secret.key=<workspace-key> \
  --namespace kube-system



Use Azure Monitor alerts for cluster health

AlertMetricThreshold
Node CPU pressurecpuUsageNanoCores> 85% for 5 min
Node memory pressurememoryWorkingSetBytes> 80% of capacity
Pod restart looprestartCount> 5 in 10 min
PVC near fullpvUsedBytes> 85% of capacity
Node not readynodeConditionNotReady > 2 min

5. Day-2 Operations Best Practices

Cluster upgrade strategy

ARO manages the control plane upgrade automatically — you control timing for worker nodes:

# Check available upgrade versions
az aro get-upgrade-versions \
  --resource-group rg-aro \
  --name aro-prod

# Schedule upgrade in maintenance window
az aro update \
  --resource-group rg-aro \
  --name aro-prod \
  --version 4.14.12


Use the EUS (Extended Update Support) channel for production clusters — it allows staying on a minor version for up to 18 months while still receiving security patches, avoiding the churn of mandatory minor version upgrades every 45 days.


Worker node upgrade — use surge capacity

# MachineConfigPool surge upgrade strategy
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfigPool
metadata:
  name: worker
spec:
  maxUnavailable: 1          # Upgrade one node at a time


Upgrade workers one node at a time to maintain application availability — pods are gracefully drained before each node reboots into the new RHCOS version.


Summary — ARO Best Practice Checklist

CategoryPractice
NetworkPrivate cluster — no public API or ingress
NetworkEgress via Azure Firewall with FQDN allow-list
NetworkDNS Private Resolver for private endpoint resolution
NetworkWorker subnet /22 or larger — never resize after
AvailabilityWorkers spread across AZs via 3 MachineSets
AvailabilityCluster autoscaler min 3 per zone
AvailabilityPremium_ZRS disks for stateful workloads
AvailabilityZone-redundant Azure Load Balancer
SecurityWorkload Identity — no credentials in pods
SecurityKey Vault + CSI driver — no base64 secrets
SecurityACR via private endpoint — no public pull
SecuritySCC restricted-v2 — no root containers
SecurityDefender for Containers enabled
ObservabilityContainer Insights → Log Analytics
ObservabilityAzure Monitor alerts on node and pod health
OperationsEUS channel for production stability
OperationsmaxUnavailable: 1 for worker upgrades

Optimize Azure Traffic Flow with UDR

Traffic Flow Through Azure Firewall via UDR

The UDR (User Defined Route) is the mechanism that forces all spoke traffic through Azure Firewall — overriding Azure’s default system routes which would otherwise send traffic directly between peered VNets, bypassing inspection entirely.


Why UDRs Are Necessary

Azure VNet peering by default creates direct routing between peered VNets — packets travel peer-to-peer without touching any intermediate device. This means without UDRs, a VM in Spoke 1 talking to a VM in Spoke 2 completely bypasses Azure Firewall:

Default behaviour (NO UDR):
  Spoke 1 VM (10.1.1.4) → Spoke 2 VM (10.2.1.4)
  System route: 10.2.0.0/16 → VNet peering (direct)
  Result: traffic bypasses firewall entirely 

With UDR applied to spoke subnet:
  Spoke 1 VM (10.1.1.4) → Spoke 2 VM (10.2.1.4)
  UDR overrides: 0.0.0.0/0 → 10.0.1.4 (Firewall private IP)
  Result: traffic hits firewall → inspected → forwarded 


The UDR wins because custom routes always override system routes — Azure’s route selection priority is custom UDR first, then BGP routes, then system routes.


Route Table Structure

A route table is an Azure resource associated with one or more subnets. Every subnet that needs inspection gets the same core UDR:

Route Table: rt-spoke1-subnets
Associated to: Spoke 1 subnet A, Spoke 1 subnet B

Routes:
  Name              Prefix          Next hop type        Next hop IP
  ─────────────────────────────────────────────────────────────────
  force-to-fw       0.0.0.0/0       Virtual Appliance    10.0.1.4


Deployed via ARM / Bicep:

resource routeTable 'Microsoft.Network/routeTables@2023-04-01' = {
  name: 'rt-spoke1-subnets'
  location: location
  properties: {
    disableBgpRoutePropagation: true   // ← critical — explained below
    routes: [
      {
        name: 'force-to-firewall'
        properties: {
          addressPrefix: '0.0.0.0/0'
          nextHopType: 'VirtualAppliance'
          nextHopIpAddress: '10.0.1.4'   // Azure Firewall private IP
        }
      }
    ]
  }
}

// Associate with spoke subnet
resource subnetAssociation 'Microsoft.Network/virtualNetworks/subnets@2023-04-01' = {
  name: 'spoke1/snet-app'
  properties: {
    addressPrefix: '10.1.1.0/24'
    routeTable: {
      id: routeTable.id
    }
  }
}



The Three Traffic Paths

Path 1 — North-South Outbound (spoke VM → internet)

Step 1: Spoke 1 VM (10.1.1.4) sends packet to 8.8.8.8
Step 2: Subnet route table consulted
UDR match: 0.0.0.0/0 → next hop 10.0.1.4 (Firewall)
Step 3: Packet arrives at Azure Firewall private IP
Step 4: Firewall evaluates application rules
Rule: allow src=10.1.0.0/16 dest=*.google.com proto=HTTPS → ALLOW
Step 5: Firewall SNATs packet
Source IP changed: 10.1.1.4 → Firewall public IP (20.x.x.x)
Step 6: Packet exits to internet from Firewall public IP
Return traffic arrives at Firewall public IP
Step 7: Firewall translates back → forwards to 10.1.1.4

SNAT is automatic for internet-bound traffic — the spoke VM’s private IP is never exposed to the internet. Azure Firewall’s public IP is the only address the internet sees.


Path 2 — North-South Inbound (internet → spoke VM)

Step 1: External client sends to Firewall public IP 20.x.x.x:443
Step 2: Firewall DNAT rule fires:
dest 20.x.x.x:443 → translated to 10.1.4.5:443 (spoke VM)
Step 3: Firewall forwards to 10.1.4.5 via VNet peering path
Step 4: Packet arrives at spoke VM — no public IP needed on VM
Step 5: VM responds to Firewall private IP (it sees FW as source)
UDR on VM subnet ensures return goes back through Firewall
Step 6: Firewall forwards return to external client

Path 3 — East-West (spoke 1 VM → spoke 2 VM)

This is the most important path for security — lateral movement between spokes must be inspected:

Step 1: Spoke 1 VM (10.1.1.4) sends packet to Spoke 2 VM (10.2.1.8)
Step 2: Spoke 1 subnet route table consulted
UDR: 0.0.0.0/0 → 10.0.1.4 (matches — more specific than system route)
Step 3: Packet arrives at Azure Firewall
Step 4: Firewall evaluates network rules
Rule: allow src=10.1.0.0/16 dest=10.2.1.8 port=443 → ALLOW
(or deny if no rule matches)
Step 5: Firewall forwards to 10.2.1.8 via peering to Spoke 2
Step 6: Spoke 2 subnet route table:
UDR: 0.0.0.0/0 → 10.0.1.4
Return traffic: 10.2.1.8 → 10.1.1.4
UDR forces return through Firewall too
Step 7: Firewall forwards return packet to Spoke 1 VM

Both directions of every connection traverse the Firewall — request and response. This is essential for stateful inspection — if only one direction went through the Firewall, the session state table would be incomplete.


disableBgpRoutePropagation — Why It Matters

Every route table has a disableBgpRoutePropagation flag. On spoke subnets this must be set to true:

disableBgpRoutePropagation: false (default)
→ VPN Gateway pushes on-premises routes into spoke effective routes
→ Spoke VM sends on-premises traffic directly to Gateway
→ Bypasses Firewall for on-premises bound traffic ❌
disableBgpRoutePropagation: true (required for spoke subnets)
→ VPN Gateway routes suppressed on spoke subnets
→ Only UDR routes active: 0.0.0.0/0 → Firewall
→ All traffic including on-premises bound goes through Firewall ✅

Forgetting this setting is one of the most common misconfiguration errors in hub and spoke deployments — on-premises traffic silently bypasses the Firewall even though the UDR looks correct.


UDR on GatewaySubnet — On-Premises to Spoke

To inspect traffic arriving from on-premises destined for spoke VNets, a UDR must also be applied to the GatewaySubnet:

Route Table: rt-gateway-subnet
Associated to: GatewaySubnet
Routes:
Name Prefix Next hop type Next hop IP
────────────────────────────────────────────────────────────────
to-spoke1 10.1.0.0/16 VirtualAppliance 10.0.1.4
to-spoke2 10.2.0.0/16 VirtualAppliance 10.0.1.4
to-spoke3 10.3.0.0/16 VirtualAppliance 10.0.1.4

Note this uses specific spoke prefixes rather than 0.0.0.0/0 — applying a default route to GatewaySubnet breaks the gateway’s ability to communicate with Azure control plane endpoints.


Effective Route Inspection

You can verify UDRs are working correctly by checking a VM’s effective routes in the Azure portal or CLI:

az network nic show-effective-route-table \
--resource-group rg-spoke1 \
--name vm-prod-01-nic \
--output table
Source State Address Prefix Next Hop Type Next Hop IP
──────── ─────── ──────────────── ────────────────── ──────────
Default Active 10.1.0.0/16 VnetLocal
Default Invalid 0.0.0.0/0 Internet ← overridden
User Active 0.0.0.0/0 VirtualAppliance 10.0.1.4 ✅
Default Active 10.0.0.0/16 VNetPeering
Default Active 10.2.0.0/16 VNetPeering

The default 0.0.0.0/0 → Internet system route shows as Invalid — it has been overridden by the custom UDR pointing to the Firewall. This confirms all traffic is force-tunnelled correctly.


Common Misconfiguration Pitfalls

Forgetting disableBgpRoutePropagation — gateway-learned routes override UDRs for on-premises prefixes, silently bypassing Firewall for hybrid traffic.

Missing return path UDR — if Spoke 2 subnet has no UDR, return traffic goes directly back to Spoke 1 via peering, creating an asymmetric routing loop that breaks TCP sessions.

Applying UDR to AzureBastionSubnet — Bastion requires direct internet connectivity for its management plane. A UDR with 0.0.0.0/0 → Firewall on AzureBastionSubnet breaks Bastion entirely. Bastion subnet must have no UDR or a very specific one that excludes Bastion management ranges.

Applying 0.0.0.0/0 UDR to GatewaySubnet — breaks gateway health probes and control plane communication. Always use specific spoke prefixes on GatewaySubnet, never a default route.

Firewall private IP not static — Azure Firewall’s private IP should be configured as static during deployment. If it changes, every UDR next-hop entry becomes invalid and traffic black-holes.


Key Takeaway

The UDR is a deceptively simple mechanism — a single route entry 0.0.0.0/0 → Virtual Appliance → 10.0.1.4 — that transforms Azure’s default direct peering behaviour into a fully inspected, security-enforced network. Applied correctly to every spoke subnet with disableBgpRoutePropagation enabled, it ensures no traffic — outbound internet, inbound DNAT, or lateral east-west — can bypass Azure Firewall, giving you complete visibility and control over your entire hub and spoke estate.

Simplify Your Azure Networking with Route Server

Azure Route Server

Azure Route Server is a fully managed service that acts as a BGP route reflector inside your hub VNet — it exchanges routes dynamically between your Network Virtual Appliances (NVAs) and Azure’s software-defined network, eliminating the need to manually maintain User Defined Routes every time your network topology changes.


The Problem It Solves

Without Route Server, every time an NVA learns a new route (a new branch office, a new subnet, a new peer) you had to manually update UDR tables on every spoke subnet:

Old approach — manual UDR maintenance:
NVA learns new branch: 192.168.50.0/24
Engineer must manually add UDR to:
- Spoke 1 subnet A route table
- Spoke 1 subnet B route table
- Spoke 2 subnet A route table
- Spoke 2 subnet B route table
- ... every subnet in every spoke
Miss one → black hole routing → outage
With Route Server:
NVA advertises 192.168.50.0/24 via BGP to Route Server
Route Server automatically programs the route
into effective routes of all peered spoke VNets
Done — zero manual intervention

How BGP Exchange Works — Step by Step

Step 1 — Route Server deploys into RouteServerSubnet

Route Server requires a dedicated subnet named exactly RouteServerSubnet with a minimum /27:

Hub VNet: 10.0.0.0/16
RouteServerSubnet: 10.0.6.0/27
→ Route Server Instance 0: 10.0.6.4
→ Route Server Instance 1: 10.0.6.5
→ Virtual IP (peering): 10.0.6.6

Route Server always deploys as two instances for high availability, both in the same subnet. Azure assigns them IPs automatically. Both instances must be peered with your NVA — you peer with each IP individually.

Route Server always uses ASN 65515 — this is fixed and cannot be changed.


Step 2 — NVA establishes eBGP sessions with both instances

Your NVA (Cisco CSR, Palo Alto VM-Series, Fortinet FortiGate, etc.) opens two BGP sessions — one to each Route Server instance. This is standard external BGP (eBGP) — the NVA and Route Server are in different ASNs:

NVA (ASN 65001) ←—eBGP—→ Route Server Instance 0 (ASN 65515, 10.0.6.4)
NVA (ASN 65001) ←—eBGP—→ Route Server Instance 1 (ASN 65515, 10.0.6.5)

Configuration on a Cisco CSR NVA:

router bgp 65001
bgp router-id 10.0.4.4
bgp log-neighbor-changes
! Peer with Route Server instance 0
neighbor 10.0.6.4 remote-as 65515
neighbor 10.0.6.4 ebgp-multihop 2
neighbor 10.0.6.4 soft-reconfiguration inbound
! Peer with Route Server instance 1
neighbor 10.0.6.5 remote-as 65515
neighbor 10.0.6.5 ebgp-multihop 2
neighbor 10.0.6.5 soft-reconfiguration inbound
! Advertise on-premises routes learned from VPN
network 192.168.0.0 mask 255.255.0.0
network 172.16.0.0 mask 255.255.0.0

The ebgp-multihop 2 is required because the NVA and Route Server are not directly connected at Layer 2 — they communicate over the VNet fabric.


Step 3 — NVA advertises routes to Route Server

The NVA tells Route Server about routes it knows — on-premises prefixes learned via VPN tunnels, SD-WAN routes, or any custom prefixes:

NVA → Route Server:
ADVERTISE 192.168.0.0/16 (on-premises HQ network)
ADVERTISE 172.16.0.0/12 (branch offices)
ADVERTISE 10.100.0.0/16 (SD-WAN overlay)

Route Server accepts these advertisements and stores them.


Step 4 — Route Server programs spoke VNet effective routes

Route Server takes the NVA-advertised routes and automatically injects them into the effective routes of every peered spoke VNet — no UDR required:

Spoke 1 VM effective routes (auto-programmed):
10.0.0.0/16 → VNet local
10.1.0.0/16 → VNet local
192.168.0.0/16 → 10.0.4.4 (NVA primary) ← from Route Server
172.16.0.0/12 → 10.0.4.4 (NVA primary) ← from Route Server
10.100.0.0/16 → 10.0.4.4 (NVA primary) ← from Route Server
0.0.0.0/0 → Internet

When a spoke VM sends traffic to 192.168.10.5 (an on-premises host), the effective route points it to the NVA, which forwards it through the appropriate VPN tunnel.


Step 5 — Route Server advertises Azure routes back to NVA

The exchange is bidirectional. Route Server tells the NVA about Azure VNet address spaces:

Route Server → NVA:
ADVERTISE 10.0.0.0/16 (hub VNet)
ADVERTISE 10.1.0.0/16 (spoke 1 VNet)
ADVERTISE 10.2.0.0/16 (spoke 2 VNet)
ADVERTISE 10.3.0.0/16 (spoke 3 VNet)

The NVA now knows all Azure prefixes and can route on-premises traffic destined for Azure correctly through its VPN tunnels — without anyone manually configuring static routes on the NVA.


Branch-to-Branch — The Key Feature

When branch-to-branch is enabled on Route Server, it becomes a route reflector between VPN Gateway and NVA, allowing on-premises sites to reach each other through Azure:

Branch A (192.168.1.0/24) ←—VPN—→ VPN Gateway
↕ BGP
Route Server
↕ BGP
Branch B (192.168.2.0/24) ←—VPN—→ NVA
With branch-to-branch ENABLED:
Route Server reflects Branch A routes → NVA → Branch B
Route Server reflects Branch B routes → VPN GW → Branch A
Result: Branch A can reach Branch B through Azure hub

This is how Azure Route Server enables transit routing — Azure becomes the backbone connecting your branch offices, without needing a separate SD-WAN overlay.

# Enable branch-to-branch
az network routeserver update \
--resource-group rg-hub-network \
--name hub-route-server \
--allow-b2b-traffic true

Active-Active NVA Pattern

Route Server is the enabler for active-active NVA deployments — both NVA instances advertise the same routes, and Route Server programs both next-hops into spoke effective routes using ECMP (Equal-Cost Multi-Path):

Both NVAs advertise: 192.168.0.0/16
Spoke VM effective routes:
192.168.0.0/16 → 10.0.4.4 (NVA primary) ← ECMP
192.168.0.0/16 → 10.0.4.5 (NVA secondary) ← ECMP
Traffic load-balanced across both NVAs
If one NVA fails → BGP session drops →
Route Server withdraws that next-hop →
All traffic shifts to remaining NVA automatically

This gives you sub-second failover without any manual intervention — the BGP hold-down timer (typically 90 seconds, tunable to as low as 1 second with BFD) triggers automatic route withdrawal.


RouteServerSubnet Requirements

PropertyRequirement
Subnet nameMust be exactly RouteServerSubnet
Minimum size/27 (32 addresses)
DedicatedNo other resources in this subnet
DelegationNone required
NSGNot supported on RouteServerSubnet
UDRNot supported on RouteServerSubnet

The restriction on NSGs and UDRs on the RouteServerSubnet is intentional — Azure manages all routing within this subnet internally, and applying UDRs would break the BGP sessions.


Route Server vs Manual UDRs — When to Use Each

ScenarioUse Route ServerUse Manual UDRs
NVA in hub for inspection
Dynamic on-premises routes via BGP
SD-WAN integration
Static force-tunnel to Azure Firewall
Simple hub with Azure Firewall only
Frequently changing branch topology
No NVA — just Azure native services

Route Server shines when you have a third-party NVA with dynamic routing requirements. If your hub uses only Azure Firewall (which does not speak BGP), stick with UDRs — Route Server adds no value without a BGP-capable NVA to peer with.


Key Limits

LimitValue
BGP peers (NVAs) per Route Server8
Routes advertised by each NVA1,000
Routes propagated to spoke VNets1,000 per VNet
Route Server ASN65515 (fixed)
NVA ASN restrictionsCannot use 65515, 65520, 12076
VNets the Route Server can peer withUnlimited (same region)

The 1,000 route limit per NVA is important for large enterprises with many branch offices — if you have more than 1,000 prefixes, use route summarisation on the NVA before advertising to Route Server.


Key Takeaway

Azure Route Server is the dynamic routing backbone of a hub and spoke network containing third-party NVAs. It replaces fragile, manually maintained UDR tables with automated BGP route exchange — the NVA advertises what it knows, Route Server programs every spoke automatically, and the whole network converges in seconds when topology changes. Combined with active-active NVAs and branch-to-branch enabled, it gives you a carrier-grade routing infrastructure entirely within Azure.

Understanding Azure Bastion: A Complete Guide

Azure Bastion

Azure Bastion is a fully managed PaaS service that provides secure RDP and SSH connectivity to your virtual machines directly through the Azure portal or native client — over TLS on port 443 — without exposing any public IP address on the VM itself.


Why Bastion Exists — The Problem It Solves

The traditional way to RDP or SSH into an Azure VM was to assign it a public IP and open port 3389 or 22 to the internet. This creates serious exposure:

Old approach:
VM has public IP 20.x.x.x → port 3389 open to internet
→ Constant brute-force attacks (thousands/day)
→ Any misconfigured NSG = immediate compromise
→ Public IP costs, management overhead
→ No session recording or audit trail
With Bastion:
VM has no public IP — completely unreachable from internet
→ Admin connects via browser to Bastion on port 443
→ Bastion proxies RDP/SSH over private VNet path
→ Session fully audited in Azure Monitor
→ No attack surface on the VM itself

How the Connection Works — Step by Step

1. Admin opens Azure portal or native client
Selects a VM → Connect → Bastion
2. Portal establishes WebSocket connection to Bastion
over HTTPS port 443 (same as normal web traffic)
→ passes through corporate firewalls without issue
3. Bastion authenticates the admin
via Azure AD / RBAC — checks if admin has
"Bastion User" role on the Bastion resource
4. Bastion opens RDP (:3389) or SSH (:22) connection
from its private NIC directly to the VM's private IP
entirely inside the VNet / peered spoke
5. RDP or SSH session streams back to browser
rendered as HTML5 — no RDP client needed
(Standard SKU supports native RDP/SSH client too)
6. Session ends → connection torn down
Audit log written to Azure Monitor / Log Analytics

The VM never sees a connection from the internet — it only sees a private IP connection from within the VNet.


AzureBastionSubnet Requirements

Bastion requires a dedicated subnet with a very specific name — Azure will refuse to deploy it anywhere else:

Subnet name: AzureBastionSubnet ← exact name required
Min size: /26 (64 addresses) ← Basic SKU minimum
Recommended: /24 or /25 ← room to scale instances
Location: Hub VNet only ← one Bastion serves all peered spokes

No other resources — VMs, firewalls, private endpoints — can share this subnet. The /26 minimum exists because Bastion deploys multiple managed instances internally and Azure reserves addresses for platform use.


Bastion SKUs

FeatureBasicStandardPremium
Browser-based RDP/SSH
Native client (RDP/SSH app)
Shareable links
IP-based connection (no Azure VM)
VNet peering support
Session scaling (instances)2 fixed2–50 scalable2–50 scalable
File transfer (upload/download)
Clipboard (copy/paste)✅ text✅ text + file✅ text + file
Session recording
Private-only Bastion (no public IP)
Kerberos authentication
Pricing~$140/month~$280/month~$470/month

Standard is the right choice for most enterprises — it adds native client support (so admins can use their local RDP or SSH client instead of the browser) and instance scaling for large teams.

Premium adds session recording — every RDP/SSH session is captured and stored in a storage account — critical for compliance environments (PCI-DSS, SOC 2) where you must prove what privileged users did during a session. It also supports fully private Bastion with no public IP at all, using a private endpoint instead.


NSG Rules Required

Two NSGs must be configured correctly — one on the AzureBastionSubnet and one on the target VM subnet:

NSG on AzureBastionSubnet

Inbound rules:

PrioritySourceDestPortProtocolActionPurpose
100InternetAny443TCPAllowAdmin HTTPS inbound
110GatewayManagerAny443TCPAllowAzure control plane
120AzureLoadBalancerAny443TCPAllowHealth probes

Outbound rules:

PrioritySourceDestPortProtocolActionPurpose
100AnyVirtualNetwork3389, 22TCPAllowRDP/SSH to VMs
110AnyAzureCloud443TCPAllowBastion diagnostics

NSG on target VM subnet

PrioritySourceDestPortProtocolActionPurpose
100VirtualNetworkAny3389TCPAllowRDP from Bastion
110VirtualNetworkAny22TCPAllowSSH from Bastion

The VM subnet must allow inbound from VirtualNetwork service tag — which includes all peered VNets including the hub where Bastion lives. Critically, there is no rule allowing port 3389 or 22 from Internet — the VM is completely shielded.


RBAC Roles for Bastion Access

Bastion access is controlled by Azure RBAC — not just network connectivity:

RoleWhat it allows
Reader on BastionSee the Bastion resource
Bastion ReaderConnect through Bastion (read-only session)
Virtual Machine Contributor on VMNeeded to initiate the connection
Bastion ContributorManage the Bastion resource

A typical setup grants an admin team the Virtual Machine Contributor role on specific spoke resource groups, plus Reader on the Bastion. They can then connect to VMs they have rights to — but cannot modify the Bastion configuration itself or connect to VMs outside their scope.


Bastion in Hub and Spoke — One Bastion for All Spokes

A single Bastion in the hub VNet reaches VMs in all peered spoke VNets — you do not need a Bastion in every spoke. This is a key cost and management advantage:

Hub VNet → AzureBastionSubnet → Azure Bastion
↓ peering allows Bastion to reach
Spoke 1 VMs (production)
Spoke 2 VMs (development)
Spoke 3 VMs (shared services)
Spoke 4 VMs (DMZ)

One Bastion instance, one Standard public IP, one monthly cost — covering your entire estate. The only requirement is that the hub-to-spoke peering has Allow gateway transit enabled on the hub side and Use remote gateways enabled on the spoke side (the same peering settings used for routing).


Key Takeaway

Azure Bastion eliminates the biggest attack surface in most Azure environments — publicly exposed management ports on VMs. By proxying all RDP and SSH through a hardened, Microsoft-managed PaaS service over HTTPS, it gives you secure, auditable VM access with no public IPs on your workloads, no VPN client requirement for browser sessions, and full RBAC control over who can connect to what.

Understanding Azure DDoS Protection Standard

Azure DDoS Protection Standard

Azure DDoS Protection Standard is a managed, always-on service that detects and mitigates volumetric, protocol, and application-layer DDoS attacks against your Azure public IP addresses — automatically, without any configuration changes during an attack.


The Three Attack Categories It Defends Against

Layer 3/4 — Volumetric attacks

These flood your network bandwidth with massive traffic volumes — UDP floods, ICMP floods, amplification attacks (DNS, NTP, memcached). Azure absorbs these at the network edge using its global 60+ Tbps scrubbing capacity, before the traffic ever reaches your VNet or gateway.

Layer 3/4 — Protocol attacks

These exhaust connection state tables on firewalls, load balancers, and gateways. SYN floods send millions of half-open TCP connections; Smurf attacks abuse ICMP broadcasts. DDoS Protection mitigates these by validating TCP handshakes and rate-limiting malformed packets at the edge.

Layer 7 — Resource layer attacks

HTTP floods, Slowloris, and application-specific attacks target your app’s compute rather than your bandwidth. DDoS Protection Standard alone does not fully mitigate Layer 7 attacks — these require Azure WAF on Application Gateway or Azure Front Door working alongside DDoS Protection. The two services are designed to be used together for full-stack protection.


How Adaptive Tuning Works

This is the core differentiator versus the free Basic tier. DDoS Protection Standard builds a per-public-IP traffic baseline using machine learning:

Normal Monday traffic profile for your VPN Gateway public IP:
- Avg 2,000 packets/sec
- Peak 8,000 packets/sec
- Protocol mix: 70% TCP, 20% UDP, 10% ICMP
- Geographic distribution: CA, US, EU
Attack detected when:
- Packets jump to 4,000,000/sec ← 500× normal
- 99% from single ASN in one region
- All UDP port 53 (DNS amplification)
Response: automatic mitigation within seconds
- Rate limit traffic matching attack signature
- Pass legitimate traffic through
- Alert via Azure Monitor

Baselines are built per public IP, per protocol, per port — so the service understands what normal looks like for your VPN Gateway vs your Application Gateway vs your load balancer, and tuning is automatic as your traffic patterns change.


DDoS Protection Tiers Compared

FeatureBasic (free)Network (Standard)IP Protection
Always-on monitoring
Automatic attack mitigation✅ basic✅ advanced✅ advanced
Adaptive ML tuning per IP
Attack analytics & metrics
Attack mitigation reports
Attack mitigation flow logs
Azure Monitor alerts
WAF policy integration
DDoS Rapid Response (Microsoft experts)
Cost protection (service credit)
ScopeAll Azure (shared)Per VNet (plan)Per public IP
PricingFree~$2,944/month + per IP~$199/IP/month

Network Protection (the classic “Standard” tier) is applied at the VNet level via a DDoS Protection Plan — every public IP in all linked VNets is automatically covered.

IP Protection is a newer, per-IP option introduced for smaller deployments where you only need to protect a handful of public IPs without paying for a full plan.


What a DDoS Protection Plan Covers

A single DDoS Protection Plan can be linked to multiple VNets across multiple subscriptions in the same tenant. This is the right model for hub and spoke — one plan at the hub subscription level covers everything:

DDoS Protection Plan (resource group: rg-network-hub)
↓ linked to
Hub VNet → VPN Gateway public IP protected
→ Azure Firewall public IP protected
→ Bastion public IP protected
Spoke 4 (DMZ) → App Gateway public IP protected
→ Load Balancer public IP protected

The first 100 public IPs are included in the plan price. Beyond 100, you pay per additional IP.


Monitoring and Alerting

During and after an attack, DDoS Protection surfaces detailed metrics in Azure Monitor:

MetricWhat it shows
Under DDoS attackBoolean — 1 if attack active on this IP
Inbound packets dropped DDoSPackets/sec being scrubbed
Inbound packets forwarded DDoSClean packets/sec passing through
Inbound bytes DDoSRaw attack volume in bytes/sec
Mitigation reasonSYN flood, UDP flood, etc.

Set an alert rule on Under DDoS attack = 1 to fire a notification to your security team or trigger a Logic App / n8n workflow the moment an attack begins.


When Should You Enable It?

Enable DDoS Protection Standard when any of these are true

Any public-facing production workload with real business impact if taken offline — an internet-facing Application Gateway, a VPN Gateway handling thousands of remote users, or a load balancer fronting a revenue-generating application — warrants the protection. The ~$3K/month cost is trivial compared to the revenue loss and incident response cost of a successful multi-hour DDoS attack.

You also need it when compliance frameworks require it. PCI-DSS, HIPAA, and ISO 27001 environments often require documented DDoS mitigation controls, and DDoS Protection Standard gives you the attack reports and flow logs needed to satisfy auditors.

The cost protection feature is a practical reason too — if an attack causes your Azure compute or bandwidth costs to spike (autoscaled VMs spun up to handle flood traffic, for example), Microsoft will credit those costs back when DDoS Protection was enabled.

You can skip it when

Dev/test environments, internal-only workloads with no public IPs, and resources entirely behind Azure Front Door or a third-party CDN that absorbs attacks upstream don’t need the plan — the Basic tier’s shared protection is sufficient, and the CDN/Front Door layer already absorbs volumetric attacks before they reach your origin.


In a Hub and Spoke Context

The recommended placement is one DDoS Protection Plan at the hub subscription, linked to the hub VNet and any spoke VNets that have public IPs (typically the DMZ spoke with App Gateway and WAF). Pair it with Azure Firewall for Layer 3/4 east-west filtering and Azure WAF on Application Gateway for Layer 7 protection, and you have defence-in-depth across all three attack categories.

Managing DNS Efficiently with Azure Private Resolver

Azure DNS Private Resolver

Azure DNS Private Resolver is a fully managed, highly available DNS service that lets your spoke VNets and on-premises networks resolve Azure private DNS zones — without deploying and managing DNS virtual machines.

Before Private Resolver existed, enterprises had to run Windows Server DNS VMs in the hub to forward queries between on-premises and Azure. Private Resolver replaces that entirely with a managed service.


The Two Endpoints

DNS Private Resolver has two endpoint types, each deployed into a dedicated delegated subnet inside the hub VNet.

Inbound Endpoint

Receives DNS queries from outside Azure — from on-premises DNS servers or spoke VNets that point their DNS server setting directly at this IP.

  • Gets a static private IP from your hub VNet subnet (e.g. 10.0.5.4)
  • Reachable over VPN Gateway or ExpressRoute from on-premises
  • On-premises DNS servers forward specific zones (e.g. *.privatelink.blob.core.windows.net) to this IP
  • Requires a dedicated /28 subnet named however you like (e.g. snet-dns-inbound)

Outbound Endpoint

Forwards DNS queries from Azure to external resolvers — typically to on-premises DNS servers for resolving internal corp domains like *.contoso.local.

  • Also gets a private IP from a dedicated /28 subnet
  • Does not receive queries directly — works only through a Forwarding Ruleset
  • Requires a dedicated subnet separate from the inbound endpoint subnet

Forwarding Ruleset

A Forwarding Ruleset is a collection of conditional forwarding rules attached to the outbound endpoint. You then link the ruleset to spoke VNets so those spokes inherit the forwarding rules automatically.

Example ruleset rules

DomainForwarding targetUse
contoso.local.192.168.1.10:53 (on-prem DNS)Resolve internal corp names
corp.contoso.com.192.168.1.10:53Resolve corp public domain internally
prod.internal.10.3.2.5:53Resolve shared-services zone
. (wildcard)168.63.129.16:53All other queries → Azure public DNS

The wildcard . rule is the catch-all — any query that doesn’t match a specific rule falls through to Azure’s public DNS resolver.


How Spoke VNets Resolve Private DNS Zones

There are two patterns depending on whether you want centralised or per-VNet control.

Pattern 1 — Custom DNS server pointing to inbound endpoint (recommended)

Each spoke VNet sets its DNS server to the inbound endpoint’s private IP (10.0.5.4) instead of the default Azure DNS (168.63.129.16):

Spoke VM queries: myaccount.blob.core.windows.net
Spoke VNet DNS setting → 10.0.5.4 (inbound endpoint)
DNS Private Resolver checks private DNS zones
Finds: myaccount.blob.core.windows.net → 10.1.8.4 (private endpoint IP)
Returns private IP — traffic stays on private network

This is set at the VNet level in Azure portal or via ARM:

{
"dhcpOptions": {
"dnsServers": ["10.0.5.4"]
}
}

Pattern 2 — Ruleset linked to spoke VNets

Link the hub’s forwarding ruleset directly to each spoke VNet. The spoke VNets keep the default Azure DNS (168.63.129.16) but inherit the conditional forwarding rules from the ruleset:

Spoke VM queries: server01.contoso.local
Azure DNS (168.63.129.16) — checks linked ruleset
Ruleset rule: contoso.local → forward to 192.168.1.10
Outbound endpoint forwards to on-premises DNS
On-premises DNS returns 192.168.10.45

Pattern 2 avoids changing the DNS server setting on every spoke and is easier to manage at scale — you just link new spokes to the existing ruleset.


Private DNS Zone Resolution Flow (Full Detail)

1. Developer VM in prod spoke queries:
mydb.privatelink.database.windows.net
2. Query goes to 10.0.5.4 (inbound endpoint)
3. DNS Private Resolver checks:
→ Is there a forwarding rule for this domain? No
→ Is there a linked private DNS zone? Yes
Zone: privatelink.database.windows.net
Record: mydb → 10.1.9.6 (private endpoint NIC IP)
4. Returns: mydb.privatelink.database.windows.net = 10.1.9.6
5. VM connects to SQL on 10.1.9.6
Traffic never leaves Azure backbone

Without DNS Private Resolver, the public DNS record for mydb.database.windows.net would resolve to the public IP, bypassing your private endpoint entirely.


Private DNS Zone Auto-Registration

When you create Azure PaaS resources with private endpoints, you link them to a private DNS zone. Common zones:

ServicePrivate DNS zone
Azure Blob Storageprivatelink.blob.core.windows.net
Azure SQL Databaseprivatelink.database.windows.net
Azure Key Vaultprivatelink.vaultcore.azure.net
Azure Container Registryprivatelink.azurecr.io
Azure Kubernetes Serviceprivatelink.{region}.azmk8s.io
Azure Monitorprivatelink.monitor.azure.com
Azure Service Busprivatelink.servicebus.windows.net

All these zones are linked to the hub VNet where DNS Private Resolver lives. Because all spokes resolve through the resolver, they automatically get the private IPs for these services.


Subnet Requirements

EndpointSubnet name (your choice)Min sizeDelegation
Inbounde.g. snet-dns-inbound/28 (16 IPs)Microsoft.Network/dnsResolvers
Outbounde.g. snet-dns-outbound/28 (16 IPs)Microsoft.Network/dnsResolvers

Both subnets must be in the hub VNet, must be separate from each other, and must not contain any other resources. The delegation is set automatically when you create the endpoint.


Before vs After Private Resolver

Before (DNS VMs)After (DNS Private Resolver)
Infrastructure2+ Windows DNS VMs in hubZero VMs — fully managed
High availabilityManual VM HA, availability setsBuilt-in, 99.99% SLA
MaintenancePatch, monitor, backup VMsNone
Conditional forwardingConfigured per-VMForwarding rulesets, linked to VNets
On-premises resolutionRequires VM reachabilityInbound endpoint IP, reachable over VPN/ER
CostVM compute + licencesPer-endpoint + per-query pricing

DNS Private Resolver is one of the clearest examples in Azure of a managed service eliminating operational overhead — the old pattern of DNS VMs in the hub was fragile, expensive, and easy to misconfigure.

Azure Firewall Rule Types in Hub and Spoke

Azure Firewall enforces three distinct rule collections processed in a strict priority order. Understanding all three is essential to designing a secure hub and spoke topology.

The three rule types are processed top to bottom — DNAT first, then network, then application. A match at any layer stops processing. If nothing matches, traffic is implicitly denied.


Rule Type 1 — DNAT Rules

Destination Network Address Translation rewrites the destination IP (and optionally port) of inbound traffic hitting the firewall’s public IP, redirecting it to a private backend inside a spoke VNet.

What it does

Internet client → Firewall public IP (52.x.x.x:443)
↓ DNAT rule fires
Rewrites destination to 10.1.4.5:443
Backend VM in production spoke

Example DNAT rules

NameProtocolSourceDest (public IP)Dest portTranslated IPTranslated port
allow-web-inboundTCP*52.10.20.3044310.1.4.5443
allow-rdp-adminTCP203.0.113.0/2452.10.20.30338910.0.3.103389
allow-api-gatewayTCP*52.10.20.3180,44310.4.2.88080

Key rules about DNAT

  • DNAT rules implicitly create a matching network rule to allow the translated traffic through — you don’t need a separate network rule for the return path.
  • DNAT only applies to inbound traffic — traffic arriving at the firewall’s public IP from outside.
  • You cannot DNAT to a broadcast or multicast address.
  • After translation, the packet is treated as if it came from the firewall’s private IP — so your backend VMs see the firewall, not the original client. Preserve source IP with SNAT if needed.

Rule Type 2 — Network Rules

Network rules are Layer 3/4 filters — they match on source IP, destination IP, port, and protocol. No payload inspection. This is the right tool for non-HTTP traffic: SQL, RDP, SMB, DNS, NTP, custom protocols.

What it does

Spoke VM (10.1.2.5) → SQL Server (10.3.4.10:1433)
Network rule: allow src=10.1.0.0/16 dest=10.3.4.10 port=1433 proto=TCP
Traffic passes — no application-layer inspection

Example network rules

NameSourceDestinationProtocolPortAction
allow-spoke-to-ad10.0.0.0/810.3.2.0/24TCP/UDP53,88,389,636Allow
allow-prod-to-sql10.1.0.0/1610.3.4.10TCP1433Allow
allow-mgmt-rdp10.0.3.0/2410.0.0.0/8TCP3389Allow
allow-ntp10.0.0.0/8*UDP123Allow
deny-dev-to-prod10.2.0.0/1610.1.0.0/16AnyAnyDeny
allow-internet-out10.0.0.0/8*TCP80,443Allow

Network rule features

IP Groups — reusable objects containing lists of IPs and CIDRs, so you don’t repeat 10.1.0.0/16, 10.2.0.0/16, 10.3.0.0/16 in every rule:

IPGroup: "all-spokes" = [10.1.0.0/16, 10.2.0.0/16, 10.3.0.0/16, 10.4.0.0/16]

FQDN in network rules — you can use FQDNs as destinations in network rules (e.g. *.windows.update.com) but only for TCP/UDP. The firewall resolves the FQDN using its DNS settings and matches on the resolved IP.

Service Tags — Microsoft-managed groups of IP ranges for Azure services:

Source: 10.0.0.0/8 → Destination: AzureMonitor → Port: 443 → Allow

Common tags: AzureCloud, AzureMonitor, Storage, Sql, WindowsUpdate, MicrosoftDefenderForEndpoint


Rule Type 3 — Application Rules

Application rules operate at Layer 7 — they can inspect the HTTP/HTTPS host header and URL path, enforce FQDN allow-lists, apply web category filtering, and (with Premium SKU) perform full TLS inspection.

What it does

Spoke VM → HTTPS request to api.github.com
Application rule: allow src=10.1.0.0/16 target=*.github.com proto=Https
Firewall checks SNI / Host header — matches rule
Traffic passes (or inspected if TLS inspection enabled)

Example application rules

NameSourceTarget FQDNsProtocolAction
allow-windows-update10.0.0.0/8*.update.microsoft.com, *.windowsupdate.comHTTP, HTTPSAllow
allow-azure-services10.0.0.0/8*.azure.com, *.core.windows.netHTTPSAllow
allow-dev-package-mgrs10.2.0.0/16*.npmjs.org, *.pypi.org, *.nuget.orgHTTPSAllow
allow-github10.1.0.0/16*.github.com, *.githubusercontent.comHTTPSAllow
deny-social-media10.0.0.0/8Web category: SocialNetworkingHTTP, HTTPSDeny
allow-all-outbound10.0.0.0/8*HTTP, HTTPSAllow

FQDN Tags — Microsoft-managed bundles

Instead of listing dozens of Microsoft service URLs manually, use built-in FQDN tags:

FQDN TagWhat it covers
WindowsUpdateAll Windows Update endpoints
WindowsDiagnosticsTelemetry and diagnostics endpoints
MicrosoftActiveProtectionServiceDefender update endpoints
AppServiceEnvironmentASE management traffic

Azure Firewall SKUs

FeatureBasicStandardPremium
DNAT rules
Network rules
Application rules
FQDN filteringLimited
Web category filtering
Threat intelligenceAlert only✅ Alert + deny✅ Alert + deny
TLS inspection
IDPS (intrusion detection)
URL filtering (path-level)
Use caseDev/testMost enterprisesHigh-security / compliance

TLS inspection (Premium only) decrypts HTTPS traffic, inspects the payload with IDPS signatures, then re-encrypts it. Requires deploying a CA certificate chain trusted by all spoke VMs — typically distributed via Group Policy or Intune.


Firewall Policy vs Classic Rules

Modern Azure Firewall uses Firewall Policy — an ARM resource that holds all rule collections and can be shared across multiple firewall instances:

Firewall Policy (parent — global rules)
↓ inheritance
Firewall Policy (child — environment-specific rules)
↓ applied to
Azure Firewall instance (hub VNet)

This lets you enforce baseline rules (e.g. deny dev→prod) at the parent policy level across all environments, while child policies add environment-specific rules. Child policies cannot override parent deny rules.


Rule Processing Priority — the Full Picture

Priority 100 (lowest number = highest priority)
DNAT collection A → rules evaluated top to bottom
DNAT collection B
Priority 200
Network collection A → IP/port rules
Network collection B
Priority 300
Application collection A → FQDN / URL rules
Application collection B
Priority 65000 (built-in)
Allow Azure infrastructure FQDNs (IMDS, DNS, etc.)
Priority 65500 (built-in)
Implicit deny all

Rule collections within each type are evaluated by priority number — lowest number wins. Within a collection, rules are evaluated top to bottom and the first match wins.

Azure VPN Gateway: A Guide to Connection Types and Benefits

What is Azure VPN Gateway?

An Azure VPN Gateway is a managed network gateway service that sends encrypted traffic between an Azure Virtual Network and an on-premises location (or another Azure VNet) over the public internet using IPsec/IKE tunnels. It’s the primary service that bridges your on-premises network to Azure in a hub and spoke topology.


Three Connection Types

Site-to-Site (S2S) connects your entire on-premises network to Azure over an IPsec/IKE tunnel. Your on-premises VPN device (router or firewall) terminates the tunnel. This is the most common type used in hub and spoke.

Point-to-Site (P2S) connects individual remote devices (laptops, phones) directly to the Azure VNet. Uses OpenVPN, SSTP, or IKEv2 protocols. No on-premises device required — just a VPN client app.

VNet-to-VNet connects two Azure VNets in different regions using the same IPsec tunnel mechanism as S2S. For same-region connections, VNet peering is cheaper and faster — VNet-to-VNet is mainly used cross-region or across subscriptions/tenants.


How It Works Internally

On-premises VPN device
↓ IPsec/IKE tunnel (encrypted)
Azure VPN Gateway (2 VM instances in GatewaySubnet)
↓ internal routing
Hub VNet → UDR propagation → Spoke VNets

The gateway always deploys as two instances for high availability. You choose between active-passive (one standby, ~10s failover) or active-active (both instances forward traffic simultaneously, faster failover).


SKUs — Full Breakdown

SKUs are grouped into generations. Generation 2 is current and recommended for all new deployments.

Generation 1 (legacy — avoid for new deployments)

SKUMax throughputS2S tunnelsP2S connectionsBGPZone-redundant
Basic100 Mbps10128
VpnGw1650 Mbps30250
VpnGw21 Gbps30500
VpnGw31.25 Gbps301,000

Generation 2 (current — recommended)

SKUMax throughputS2S tunnelsP2S connectionsBGPZone-redundant
VpnGw1650 Mbps30250
VpnGw21 Gbps30500
VpnGw31.25 Gbps301,000
VpnGw45 Gbps1005,000
VpnGw510 Gbps10010,000
VpnGw1AZ650 Mbps30250
VpnGw2AZ1 Gbps30500
VpnGw3AZ1.25 Gbps301,000
VpnGw4AZ5 Gbps1005,000
VpnGw5AZ10 Gbps10010,000

The AZ suffix means the gateway is deployed across Availability Zones — its instances span physically separate datacentre buildings, protecting against a full zone failure. This is the right choice for production workloads with strict uptime requirements.


SKU Selection Guide

ScenarioRecommended SKU
Dev/test only, no BGP neededBasic
Small org, <30 branch officesVpnGw1AZ
Mid-size enterpriseVpnGw2AZ or VpnGw3AZ
Large enterprise, many tunnelsVpnGw4AZ
Very high throughput (10 Gbps)VpnGw5AZ
High SLA required in productionAny AZ SKU

Key Concepts to Know

BGP (Border Gateway Protocol) — enables dynamic route exchange between Azure and your on-premises router. Without BGP, you must manually define every on-premises subnet in the Local Network Gateway. With BGP, routes are exchanged automatically. Required for active-active configurations and most enterprise setups.

GatewaySubnet — a dedicated subnet in your hub VNet that must be named exactly GatewaySubnet. Minimum /27 (32 addresses), recommended /26 or larger for future gateway coexistence (VPN + ExpressRoute). No other resources should be placed in this subnet.

Local Network Gateway — an Azure resource that represents your on-premises VPN device. You define its public IP address and the address space of your on-premises network here.

Active-Active mode — both gateway instances are active simultaneously, each with its own public IP. Your on-premises VPN device must support two tunnels. Provides near-zero downtime failover and higher aggregate throughput.

IKE versions — the gateway supports IKEv1 and IKEv2. IKEv2 is preferred — it’s faster to negotiate, more secure, and required for P2S with IKEv2 clients.


VPN Gateway vs ExpressRoute Gateway

VPN GatewayExpressRoute Gateway
TransportPublic internet (encrypted)Private MPLS circuit (unencrypted at layer)
Max throughput10 Gbps (VpnGw5AZ)Up to 100 Gbps (UltraPerformance)
LatencyVariable (internet)Consistent, low latency
CostLowerHigher (circuit + gateway)
Use caseMost enterprisesFinancial, healthcare, high-compliance

In many enterprise deployments both coexist in the same GatewaySubnet — ExpressRoute as the primary path, VPN as the failover.

Azure Hub and Spoke Network Design Explained

Here’s the Azure Hub and Spoke network architecture — the foundational enterprise network pattern on Azure. I’ll show it in two diagrams: the overall topology first, then the hub internals in detail.

The topology shows the hub as the central control point, with all spoke VNets peered to it. Now here’s a closer look at what lives inside the hub and how traffic flows through it.

Azure Hub and Spoke — Key Design Principles


Why Hub and Spoke?

Hub and spoke is the recommended enterprise network topology for Azure. Instead of each team or workload managing its own connectivity and security, all shared services live in one central hub VNet, and workloads live in isolated spoke VNets peered to it.

Every spoke talks to the world THROUGH the hub — never directly.

The Hub VNet — what lives inside

The hub is the security and connectivity control plane. It contains no workloads — only shared infrastructure:

  • VPN Gateway / ExpressRoute Gateway — the on-premises bridge, placed in a dedicated GatewaySubnet. All hybrid traffic enters and exits here.
  • Azure Firewall — placed in AzureFirewallSubnet, it inspects all east-west (spoke-to-spoke) and north-south (internet/on-prem) traffic. Every spoke uses a User Defined Route (UDR) pointing 0.0.0.0/0 to the firewall’s private IP.
  • Azure Bastion — placed in AzureBastionSubnet, it provides browser-based RDP/SSH to any VM in any peered spoke without requiring public IPs on the VMs.
  • Route Server — exchanges BGP routes with Network Virtual Appliances (NVAs) so dynamic routing updates propagate automatically across all spokes.
  • DNS Private Resolver — centralises DNS resolution for all private DNS zones, so every spoke resolves *.privatelink.azure.com correctly through the hub.
  • DDoS Protection Standard — applied at the subscription level, protects all public IPs across hub and spokes from volumetric attacks.

The Spoke VNets — what lives inside

Each spoke is an isolated workload boundary:

SpokeTypical contentsCIDR
ProductionApp VMs, AKS, SQL MI, App Service10.1.0.0/16
DevelopmentDev/test workloads, lower SKUs10.2.0.0/16
Shared servicesActive Directory DCs, monitoring agents10.3.0.0/16
DMZ / perimeterApp Gateway, WAF, API Management10.4.0.0/16

Spokes cannot talk to each other directly — traffic must traverse the hub firewall, giving you full inspection and control of lateral movement.


Traffic flow rules

All routing is forced through the hub firewall via UDRs applied to every spoke subnet:

Spoke VM → UDR (0.0.0.0/0 → Firewall IP)
Azure Firewall (inspect, allow/deny)
Destination (internet / on-prem / other spoke)

This means even spoke-to-spoke traffic — for example, production VM calling a shared services VM — travels hub → firewall → hub → destination, giving you a full audit trail.


Address space planning

Non-overlapping CIDRs are mandatory — VNet peering fails if address spaces overlap:

Hub VNet: 10.0.0.0/16
GatewaySubnet: 10.0.0.0/27 (min /27 for gateway)
AzureFirewallSubnet: 10.0.1.0/26 (min /26)
AzureBastionSubnet: 10.0.2.0/26 (min /26)
RouteServerSubnet: 10.0.3.0/27 (min /27)
Spoke 1 (prod): 10.1.0.0/16
Spoke 2 (dev): 10.2.0.0/16
Spoke 3 (shared svc): 10.3.0.0/16
Spoke 4 (DMZ): 10.4.0.0/16

When to use Azure Virtual WAN instead

Hub and spoke with manual VNet peering works well up to ~10 spokes. Beyond that, consider Azure Virtual WAN — a Microsoft-managed hub that automatically handles routing, peering, and gateway scaling across dozens of spokes and multiple regions, at the cost of less customisation flexibility.