Azure DNS Private Resolver: Eliminating Custom DNS VMs in Hybrid Networks
ToC
Introduction: The DNS VM Tax
Every enterprise landing zone I have walked into that has been running in Azure for more than a year has the same problem: a pair of Windows Server DNS VMs sitting in the hub VNet, serving as conditional forwarders between on-premises and Azure. Sometimes there are four of them — two per region. They run 24/7, consume compute capacity, need OS patching, require backup policies, and generate alerts when disk fills up. The DNS forwarder VM is essentially a toll booth you built because there was no managed alternative.
Until Azure DNS Private Resolver.
Released to general availability in September 2022, Azure DNS Private Resolver is a fully managed, zone-redundant service that provides conditional DNS forwarding in both directions: from on-premises into Azure private DNS zones, and from Azure VMs out to on-premises authoritative DNS servers. No VMs to patch. No failover to configure. It scales automatically and costs a fraction of what you spend on DNS forwarder VMs.
In this deep-dive, I will show you exactly how it works, how to deploy it properly in a hub-and-spoke topology, and the specific production gotchas that will catch you off guard if you migrate from a VM-based DNS architecture without reading the fine print.
Architecture Overview
Azure DNS Private Resolver has three core concepts you need to understand before touching the portal or CLI.
The Resolver Resource
The resolver itself is a regional resource deployed into a VNet. You can only have one resolver per VNet, but that resolver serves multiple endpoints. The resolver is the parent container — it holds inbound endpoints and outbound endpoints, and it delegates subnets to the Microsoft.Network/dnsResolvers service.
Inbound Endpoints
An inbound endpoint is an IP address within your VNet address space that accepts DNS queries from outside Azure. This is the target you configure in your on-premises DNS conditional forwarder. When your on-premises AD-integrated DNS server needs to resolve `privatelink.blob.core.windows.net` or `myapp.internal.azure.com`, it forwards those queries to the inbound endpoint IP.
The inbound endpoint requires a dedicated subnet delegated exclusively to Microsoft.Network/dnsResolvers — it cannot host any other resources. Minimum size is /28, but /26 is the standard recommendation for production.
Outbound Endpoints and Forwarding Rulesets
An outbound endpoint is the source IP used when Azure forwards DNS queries out toward your on-premises DNS servers. The outbound endpoint is paired with a DNS Forwarding Ruleset — a collection of rules mapping domain suffixes to upstream DNS server IPs.
For example:
- `corp.contoso.com.` — forward to `10.10.1.10` (on-premises AD DNS)
- `legacy.internal.` — forward to `10.10.1.11`
- Any unmatched queries fall back to Azure DNS automatically
A Forwarding Ruleset can be linked to multiple VNets. This is the key architectural advantage: one ruleset covers your entire hub-and-spoke topology without per-spoke configuration. This single design decision eliminates most of the operational overhead that comes with VM-based DNS forwarders.
Resolution Flow
For on-premises to Azure (inbound path):
- On-premises DNS has a conditional forwarder: `privatelink.blob.core.windows.net` points to the inbound endpoint IP (e.g., `10.100.0.10`)
- Query arrives at the inbound endpoint
- Azure DNS Private Resolver consults Private DNS zones linked to the resolver VNet
- Returns the private IP of the storage account private endpoint
For Azure to on-premises (outbound path):
- Azure VM in a spoke VNet queries `db01.corp.contoso.com`
- Azure DNS is consulted, finds the VNet is linked to a forwarding ruleset
- The ruleset has a matching rule: `corp.contoso.com.` forwards to `10.10.1.10`
- Query is forwarded via the outbound endpoint to the on-premises DNS server
- Response flows back to the VM
The full architecture is documented in the Azure DNS Private Resolver Overview and the endpoints and rulesets reference.
Inbound Endpoint: On-Premises to Azure
Subnet Planning
Before creating an inbound endpoint, plan your subnets. The inbound subnet must be exclusively delegated to `Microsoft.Network/dnsResolvers` and cannot host any other resources. Minimum size is /28 (16 addresses), with /26 recommended for production.
I reserve separate subnets for inbound and outbound endpoints in every hub I design:
10.100.0.0/26 - snet-dns-resolver-inbound (delegated to Microsoft.Network/dnsResolvers)
10.100.0.64/26 - snet-dns-resolver-outbound (delegated to Microsoft.Network/dnsResolvers)Static vs Dynamic IP Assignment
By default, the inbound endpoint IP is assigned dynamically from the subnet range. In production, use static IP assignment so that your on-premises conditional forwarder always points to a predictable address. If the IP changes and you have not updated your on-premises forwarder, DNS breaks silently for your entire hybrid workforce.
# Create DNS Private Resolver in the hub VNet
az dns-resolver create \
--resource-group rg-networking-hub \
--name dns-resolver-hub-eastus \
--location eastus \
--id "/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub-eastus"
# Create inbound endpoint with static IP assignment
az dns-resolver inbound-endpoint create \
--resource-group rg-networking-hub \
--dns-resolver-name dns-resolver-hub-eastus \
--name inbound-ep \
--location eastus \
--ip-configurations "[{privateIpAddress:'10.100.0.10',privateIpAllocationMethod:'Static',id:'/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub-eastus/subnets/snet-dns-inbound'}]"After creation, verify the assigned IP:
az dns-resolver inbound-endpoint show \
--resource-group rg-networking-hub \
--dns-resolver-name dns-resolver-hub-eastus \
--name inbound-ep \
--query "properties.ipConfigurations[0].privateIpAddress" \
--output tsvOutbound Endpoint and Forwarding Rulesets
Creating the Forwarding Ruleset
A Forwarding Ruleset is an independent ARM resource — not a child of the resolver itself. This design is intentional: one ruleset can be shared across multiple VNets. The ruleset references the outbound endpoint by resource ID.
# Create outbound endpoint
az dns-resolver outbound-endpoint create \
--resource-group rg-networking-hub \
--dns-resolver-name dns-resolver-hub-eastus \
--name outbound-ep \
--location eastus \
--subnet-id "/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub-eastus/subnets/snet-dns-outbound"
# Create forwarding ruleset linked to the outbound endpoint
az dns-resolver forwarding-ruleset create \
--resource-group rg-networking-hub \
--name frs-hub-eastus \
--location eastus \
--dns-resolver-outbound-endpoints "[{id:'/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/dnsResolvers/dns-resolver-hub-eastus/outboundEndpoints/outbound-ep'}]"Adding Forwarding Rules
Each rule maps a domain suffix to one or more target DNS servers. Specify multiple servers for resilience:
# Forward on-premises domain to on-prem DNS servers
az dns-resolver forwarding-rule create \
--resource-group rg-networking-hub \
--forwarding-ruleset-name frs-hub-eastus \
--name rule-corp-contoso \
--domain-name "corp.contoso.com." \
--dns-forwarding-rule-state Enabled \
--target-dns-servers "[{ipAddress:'10.10.1.10',port:53},{ipAddress:'10.10.1.11',port:53}]"
# Forward the root domain as well
az dns-resolver forwarding-rule create \
--resource-group rg-networking-hub \
--forwarding-ruleset-name frs-hub-eastus \
--name rule-contoso-root \
--domain-name "contoso.com." \
--dns-forwarding-rule-state Enabled \
--target-dns-servers "[{ipAddress:'10.10.1.10',port:53},{ipAddress:'10.10.1.11',port:53}]"Critical: Domain names in forwarding rules must end with a trailing dot (`.`). The Azure portal adds this automatically. The Azure CLI and Bicep do not. If you omit the trailing dot, you will get confusing validation errors or rules that silently never match.
Linking Rulesets to VNets
A ruleset link applies the forwarding rules to DNS queries from VMs in a specific VNet:
az dns-resolver vnet-link create \
--resource-group rg-networking-hub \
--forwarding-ruleset-name frs-hub-eastus \
--name link-vnet-spoke01 \
--virtual-network "{id:'/subscriptions/<sub>/resourceGroups/rg-spoke01/providers/Microsoft.Network/virtualNetworks/vnet-spoke01'}"Step-by-Step Deployment
Here is a complete Bicep deployment for a hub resolver. This is production-grade, parameterized, and follows the Azure Cloud Adoption Framework naming conventions.
// dns-resolver.bicep
@description('Name of the hub virtual network')
param vnetHubName string = 'vnet-hub-eastus'
@description('Resource group of the hub VNet')
param vnetHubRg string = 'rg-networking-hub'
@description('Inbound subnet resource ID')
param inboundSubnetId string
@description('Outbound subnet resource ID')
param outboundSubnetId string
@description('On-premises DNS server IPs for forwarding')
param onpremDnsServers array = [
{ ipAddress: '10.10.1.10', port: 53 }
{ ipAddress: '10.10.1.11', port: 53 }
]
@description('On-premises domain suffix to conditionally forward')
param onpremDomainSuffix string = 'corp.contoso.com.'
resource hubVnet 'Microsoft.Network/virtualNetworks@2023-06-01' existing = {
name: vnetHubName
scope: resourceGroup(vnetHubRg)
}
resource dnsResolver 'Microsoft.Network/dnsResolvers@2022-07-01' = {
name: 'dns-resolver-hub'
location: resourceGroup().location
properties: {
virtualNetwork: {
id: hubVnet.id
}
}
}
resource inboundEndpoint 'Microsoft.Network/dnsResolvers/inboundEndpoints@2022-07-01' = {
parent: dnsResolver
name: 'inbound-ep'
location: resourceGroup().location
properties: {
ipConfigurations: [
{
subnet: { id: inboundSubnetId }
privateIpAllocationMethod: 'Dynamic'
}
]
}
}
resource outboundEndpoint 'Microsoft.Network/dnsResolvers/outboundEndpoints@2022-07-01' = {
parent: dnsResolver
name: 'outbound-ep'
location: resourceGroup().location
properties: {
subnet: { id: outboundSubnetId }
}
}
resource forwardingRuleset 'Microsoft.Network/dnsForwardingRulesets@2022-07-01' = {
name: 'frs-hub-eastus'
location: resourceGroup().location
properties: {
dnsResolverOutboundEndpoints: [
{ id: outboundEndpoint.id }
]
}
}
resource forwardingRule 'Microsoft.Network/dnsForwardingRulesets/forwardingRules@2022-07-01' = {
parent: forwardingRuleset
name: 'rule-onprem'
properties: {
domainName: onpremDomainSuffix
forwardingRuleState: 'Enabled'
targetDnsServers: onpremDnsServers
}
}
output inboundEndpointIp string = inboundEndpoint.properties.ipConfigurations[0].privateIpAddress
output resolverResourceId string = dnsResolver.idDeploy it:
az deployment group create \
--resource-group rg-networking-hub \
--template-file dns-resolver.bicep \
--parameters vnetHubName=vnet-hub-eastus \
inboundSubnetId="/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub-eastus/subnets/snet-dns-inbound" \
outboundSubnetId="/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub-eastus/subnets/snet-dns-outbound"After deployment, configure on-premises Windows Server DNS with conditional forwarders pointing to the inbound endpoint IP:
# Run on your on-premises DNS server (replace 10.100.0.10 with actual inbound endpoint IP)
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.blob.core.windows.net" `
-MasterServers @("10.100.0.10") `
-PassThru
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.vaultcore.azure.net" `
-MasterServers @("10.100.0.10") `
-PassThru
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.database.windows.net" `
-MasterServers @("10.100.0.10") `
-PassThruHub-and-Spoke Integration
This is where the architecture becomes operationally elegant. In a hub-and-spoke topology, you deploy the DNS Private Resolver only in the hub VNet. You then link the forwarding ruleset to each spoke VNet. No resolver in the spokes — they inherit DNS forwarding behaviour through the ruleset link.
[Spoke VNet A] --linked to ruleset--+
[Spoke VNet B] --linked to ruleset--+-- [Hub Resolver] --outbound--> [On-Prem DNS]
[Spoke VNet C] --linked to ruleset--+Requirements: spoke VNets must be VNet-peered to the hub, and the hub inbound/outbound subnets must be reachable from on-premises via ExpressRoute or VPN.
The Most Common Misconfiguration
I see this repeatedly in every workshop: engineers set the spoke VNet custom DNS server setting to the inbound endpoint IP of the hub resolver. This is wrong and will break DNS. Do not change DNS server settings on spoke VNets when using DNS Private Resolver.
The forwarding ruleset linkage works at the Azure DNS layer. It intercepts queries processed by Azure DNS (168.63.129.16) and applies forwarding rules transparently. Overriding the VNet DNS server to the inbound endpoint creates circular resolution loops for Azure-internal hostnames.
Correct configuration for all VNets:
- DNS server setting: Leave as Default (Azure-provided) on all VNets
- Forwarding Ruleset: Link it to all VNets that need outbound conditional forwarding
- Private DNS zones: Link the `privatelink.*` zones to the hub VNet for inbound resolution
Adding New Spokes
When onboarding a new spoke VNet, add it to the DNS fabric with a single command:
SPOKE_VNET_ID=$(az network vnet show \
--resource-group rg-spoke-new \
--name vnet-spoke-new \
--query id -o tsv)
az dns-resolver vnet-link create \
--resource-group rg-networking-hub \
--forwarding-ruleset-name frs-hub-eastus \
--name "link-vnet-spoke-new" \
--virtual-network "{id:'$SPOKE_VNET_ID'}"Compare this to the old approach: updating custom DNS server settings on the VNet, waiting for DHCP lease renewals, and restarting VMs. The DNS Private Resolver approach is measurably faster and less error-prone.
Production Gotchas
1. One Resolver Per VNet
You cannot deploy two resolvers in the same VNet. In large organisations where different teams manage different hub components without coordination, this causes deployment failures. Use Azure Policy to enforce that each designated hub VNet has at most one DNS resolver.
2. NSG Rules on Resolver Subnets Are Critical
By default, resolver subnets reject DNS traffic from outside the VNet unless you explicitly allow UDP/53 and TCP/53 inbound. I have seen production migrations fail silently for this reason — the error on the on-premises side is just a generic DNS timeout with no indication the NSG is dropping packets.
# Allow on-premises DNS queries to the inbound endpoint subnet
az network nsg rule create \
--resource-group rg-networking-hub \
--nsg-name nsg-snet-dns-inbound \
--name Allow-OnPrem-DNS-UDP \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Udp \
--source-address-prefixes 10.10.0.0/16 \
--destination-port-ranges 53
# Also allow TCP/53 for DNS responses larger than 512 bytes
az network nsg rule create \
--resource-group rg-networking-hub \
--nsg-name nsg-snet-dns-inbound \
--name Allow-OnPrem-DNS-TCP \
--priority 101 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes 10.10.0.0/16 \
--destination-port-ranges 53For the outbound subnet, add egress rules allowing UDP/53 and TCP/53 from the outbound subnet CIDR to your on-premises DNS server IPs.
3. Private DNS Zones Must Be Linked to the Hub VNet
Azure DNS Private Resolver can only resolve Private DNS zones that are linked to the resolver's VNet. If you have a Private DNS zone `privatelink.blob.core.windows.net` linked only to a spoke VNet, on-premises queries hitting the hub inbound endpoint will return NXDOMAIN.
The correct pattern: link all `privatelink.*` Private DNS zones to the hub VNet. This is the standard design described in the Azure Private Endpoint DNS Integration documentation.
4. Maximum Forwarding Rules Per Ruleset
Each forwarding ruleset supports a maximum of 1,000 forwarding rules. For most enterprises this is ample. Organizations with many acquired subsidiaries and complex split-horizon configurations can approach this ceiling. Consolidate where possible using wildcard rules: `*.contoso.com.` covers all subdomains and counts as one rule.
5. Resolver Does Not Forward to 168.63.129.16
You cannot specify `168.63.129.16` (the Azure magic IP) as a forwarding rule target. The resolver falls back to Azure DNS automatically for any suffix not matched by a forwarding rule. Attempting to add this IP as a target returns a validation error.
6. Subnet Delegation Requires Endpoint Deletion to Undo
Once a subnet is delegated to `Microsoft.Network/dnsResolvers`, you cannot remove the delegation while endpoints exist. If you need to reconfigure subnet assignments, delete the resolver endpoints first, remove the delegation, then recreate.
Monitoring and Diagnostics
Enabling Diagnostic Logs
Enable diagnostic logs on the resolver to capture DNS query telemetry. Use the portal under Diagnostic settings, or CLI:
az monitor diagnostic-settings create \
--resource "/subscriptions/<sub>/resourceGroups/rg-networking-hub/providers/Microsoft.Network/dnsResolvers/dns-resolver-hub-eastus" \
--workspace "/subscriptions/<sub>/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/law-hub" \
--name "diag-dns-resolver" \
--logs '[{"category":"DNSQueryLogs","enabled":true,"retentionPolicy":{"enabled":true,"days":90}}]'Two log categories are available:
- DNSQueryLogs: Individual query logs with source IP, queried name, record type, response code, and latency
- AuditLogs: Control plane events such as endpoint creation and ruleset changes
Key KQL Queries
// Queries returning NXDOMAIN - investigate potential DNS zone or link misconfigurations
AzureDiagnostics
| where Category == "DNSQueryLogs"
| where ResourceType == "DNSRESOLVERS"
| where ResultCode_s == "NXDOMAIN"
| summarize Count = count() by QueryName_s, ClientIP_s
| sort by Count desc
| take 50// Top inbound queries from on-premises
AzureDiagnostics
| where Category == "DNSQueryLogs"
| where Direction_s == "Inbound"
| summarize Count = count() by QueryName_s
| sort by Count desc
| take 20// Outbound forwarding rule usage over time
AzureDiagnostics
| where Category == "DNSQueryLogs"
| where Direction_s == "Outbound"
| summarize Count = count() by ForwardingRuleName_s, bin(TimeGenerated, 1h)
| render timechartConnectivity Testing
Test inbound resolution from on-premises:
# From an on-premises machine - target the inbound endpoint IP explicitly
nslookup mysa.privatelink.blob.core.windows.net 10.100.0.10Test outbound resolution from an Azure VM in a linked spoke VNet:
# From Azure VM - should resolve via the outbound endpoint to on-prem DNS
nslookup db01.corp.contoso.comIf nslookup from on-premises times out, work through this checklist:
- Network route exists from on-premises to the inbound subnet (via ExpressRoute or VPN)
- NSG on inbound subnet allows UDP/53 and TCP/53 from the on-premises source IP range
- All required `privatelink.*` Private DNS zones are linked to the hub VNet
- The resolver provisioning state is Succeeded (check with `az dns-resolver show`)
Conclusion
Azure DNS Private Resolver is one of those Azure services that, once you understand it, makes your previous architecture look unnecessarily complex. Two dedicated subnets in the hub VNet, a fifteen-minute deployment, and a single CLI command to link each new spoke VNet — that replaces a pair of patched DNS VMs, their availability sets, custom monitoring, and the on-call escalation when one of them ran out of disk space.
The two things I always emphasize in training:
- Do not set custom DNS on spoke VNets — let the ruleset link work at the Azure DNS layer
- Link all Private DNS zones to the hub VNet — not just to the spoke where the private endpoint lives
If you are running custom DNS forwarder VMs in your Azure hub today, migrating to Azure DNS Private Resolver is a weekend project that pays operational dividends every month. Start with a test environment, validate inbound and outbound flows, then cut over production during a maintenance window.
For further reading: