What you are building
Two flows need to work:
- On-premises → Azure: on-premises servers resolve
privatelink.*names (Storage, SQL, Key Vault private endpoints) to private IPs. - Azure → on-premises: Azure VMs resolve
corp.contoso.comthrough on-premises DNS servers.
Many estates use a pair of Windows DNS VMs as forwarders for this. Azure DNS Private Resolver is a managed service that does the same job with no VMs to patch.
| Component | Purpose |
|---|---|
| Resolver | Lives in the hub VNet |
| Inbound endpoint | Private IP that on-premises DNS forwards to |
| Outbound endpoint | Source of queries going to on-premises DNS |
| Forwarding ruleset | Rules such as "corp.contoso.com → 10.10.0.4, 10.10.0.5", linked to VNets |
Prerequisites
- A hub VNet connected to on-premises (VPN or ExpressRoute).
- Two empty subnets in the hub, each delegated to
Microsoft.Network/dnsResolvers. Use /28 or larger. Check the documentation for the current minimum. - Private DNS zones (
privatelink.blob.core.windows.netand so on) linked to the hub VNet.
az extension add --name dns-resolver --upgrade
az network vnet subnet create -g rg-hub --vnet-name vnet-hub -n snet-dns-in \
--address-prefixes 10.0.10.0/28 --delegations Microsoft.Network/dnsResolvers
az network vnet subnet create -g rg-hub --vnet-name vnet-hub -n snet-dns-out \
--address-prefixes 10.0.10.16/28 --delegations Microsoft.Network/dnsResolvers
1. Create the resolver and endpoints
VNET_ID=$(az network vnet show -g rg-hub -n vnet-hub --query id -o tsv)
IN_SUBNET=$(az network vnet subnet show -g rg-hub --vnet-name vnet-hub -n snet-dns-in --query id -o tsv)
OUT_SUBNET=$(az network vnet subnet show -g rg-hub --vnet-name vnet-hub -n snet-dns-out --query id -o tsv)
az dns-resolver create -g rg-hub -n dnspr-hub --location westeurope --id $VNET_ID
az dns-resolver inbound-endpoint create -g rg-hub --dns-resolver-name dnspr-hub -n in-ep \
--location westeurope \
--ip-configurations "[{private-ip-allocation-method:Dynamic,id:$IN_SUBNET}]"
az dns-resolver outbound-endpoint create -g rg-hub --dns-resolver-name dnspr-hub -n out-ep \
--location westeurope --id $OUT_SUBNET
Get the inbound IP for the on-premises forwarders:
az dns-resolver inbound-endpoint show -g rg-hub --dns-resolver-name dnspr-hub -n in-ep \
--query "ipConfigurations[0].privateIpAddress" -o tsv
Tip
Parameter names in the dns-resolver extension have changed between versions. If a command fails, check
az dns-resolver inbound-endpoint create --helpfor your build.
2. On-premises → Azure
On the on-premises DNS servers, add conditional forwarders for each Private Link zone you use, pointing at the inbound endpoint IP. On Windows DNS:
Add-DnsServerConditionalForwarderZone -Name "blob.core.windows.net" -MasterServers 10.0.10.4 -ReplicationScope Forest
Add-DnsServerConditionalForwarderZone -Name "database.windows.net" -MasterServers 10.0.10.4 -ReplicationScope Forest
Add-DnsServerConditionalForwarderZone -Name "vault.azure.net" -MasterServers 10.0.10.4 -ReplicationScope Forest
Forward the public zone name (blob.core.windows.net), not privatelink.blob.core.windows.net. The public name returns a CNAME to the privatelink name, and Azure DNS answers that from the linked private zone.
3. Azure → on-premises
OUT_EP=$(az dns-resolver outbound-endpoint show -g rg-hub --dns-resolver-name dnspr-hub -n out-ep --query id -o tsv)
az dns-resolver forwarding-ruleset create -g rg-hub -n frs-hub --location westeurope \
--outbound-endpoints "[{id:$OUT_EP}]"
az dns-resolver forwarding-rule create -g rg-hub --ruleset-name frs-hub -n corp \
--domain-name "corp.contoso.com." --forwarding-rule-state Enabled \
--target-dns-servers "[{ip-address:10.10.0.4,port:53},{ip-address:10.10.0.5,port:53}]"
The domain name ends with a dot. Then link the ruleset to every VNet that needs to resolve on-premises names (the hub and each spoke):
az dns-resolver vnet-link create -g rg-hub --ruleset-name frs-hub -n link-spoke-app \
--id $(az network vnet show -g rg-spoke-app -n vnet-spoke-app --query id -o tsv)
Spoke VNets keep using Azure-provided DNS (168.63.129.16). The ruleset link is what sends corp.contoso.com to on-premises. You don't need custom DNS servers on the spokes.
4. Test
From an on-premises server:
Resolve-DnsName mystorageacct.blob.core.windows.net
# expect a CNAME to mystorageacct.privatelink.blob.core.windows.net and a 10.x address
From an Azure VM in a spoke:
nslookup fileserver01.corp.contoso.com
dig +short mystorageacct.blob.core.windows.net
Troubleshooting
| Symptom | Check |
|---|---|
| On-premises gets the public IP | Conditional forwarder missing, or the private zone not linked to the hub VNet |
| On-premises gets a timeout | Firewall or NSG blocking UDP/TCP 53 to the inbound endpoint subnet |
| Azure VM can't resolve corp names | Ruleset not linked to that VNet, or the VNet uses custom DNS servers that bypass Azure DNS |
| Works for some spokes only | Each spoke needs its own ruleset link |
Migrating from forwarder VMs
- Build the resolver alongside the existing VMs.
- Add the inbound IP as an extra forwarder on-premises, and test.
- Switch spoke VNets from custom DNS (the VM IPs) back to Azure-provided DNS and link the ruleset. Each VM picks up the change at its next DHCP renewal or reboot.
- Remove the old forwarder VMs once the queries hitting them drop to zero.
Checklist
- Two delegated subnets in the hub
- Inbound and outbound endpoints created
- On-premises conditional forwarders for each Private Link zone
- Forwarding ruleset with on-premises domains, linked to all VNets
- Spokes use Azure-provided DNS
- Both directions tested