[LZ 4.2] Wiring Name Resolution End to End

The build sheet for name resolution: the DNS private resolver and its inbound endpoint, the private DNS zones, the on-premises conditional forwarders, the catch-all remediation discovery flagged, and a private endpoint proven to resolve privately from both the spoke and on-premises.

This build sheet wires name resolution end to end: the private DNS resolver in the hub, the private DNS zones that back private endpoints, the on-premises conditional forwarders that point at the resolver, and the remediation of the catch-all forwarder that discovery flagged. It closes with the test that actually matters, resolving a real private endpoint from both a spoke and on-premises. Name resolution is where an otherwise correct network fails silently months later, so the validation here is not optional.

Prerequisites: the hub and its reserved snet-dnsresolver-inbound subnet from the connectivity build sheet, administrative access to your on-premises DNS servers, and the discovery finding that lists any existing broad forwarder or stub zone for Microsoft domains.

Step 1: Create the DNS private resolver and its inbound endpoint

The resolver binds to the hub virtual network and exposes an inbound endpoint that on-premises will forward to. In the portal you create a DNS Private Resolver against the hub, then add an inbound endpoint and select the snet-dnsresolver-inbound subnet, which delegates the subnet to the resolver as part of the selection. The consolidated CLI does the same, delegating the subnet explicitly first.

RG=rg-connectivity-prod-wus2-001

az network vnet subnet update -g $RG --vnet-name vnet-hub-prod-wus2-001 \
  -n snet-dnsresolver-inbound --delegations Microsoft.Network/dnsResolvers

az dns-resolver create -g $RG -n dnspr-hub-prod-wus2-001 -l westus2 \
  --virtual-network vnet-hub-prod-wus2-001

SUBNET_ID=$(az network vnet subnet show -g $RG \
  --vnet-name vnet-hub-prod-wus2-001 -n snet-dnsresolver-inbound --query id -o tsv)

az dns-resolver inbound-endpoint create -g $RG \
  --dns-resolver-name dnspr-hub-prod-wus2-001 -n inbound -l westus2 \
  --ip-configurations "[{\"subnet\":{\"id\":\"$SUBNET_ID\"},\"private-ip-allocation-method\":\"Dynamic\"}]"

Record the inbound endpoint’s private IP, because it is the address every on-premises forwarder will target. It will fall inside the snet-dnsresolver-inbound range, so in the worked example it lands in 10.40.0.192/28.

Each Azure service reached by a private endpoint has a specific privatelink zone name, and the names are not arbitrary, so use the exact ones for the services you actually use. Create the zone, then link it to the hub with registration disabled, since these zones hold private endpoint records rather than auto-registered virtual machine names. The pattern below is for blob storage; repeat it for each service, such as the Key Vault and SQL zones.

ServicePrivate DNS zone
Blob storageprivatelink.blob.core.windows.net
Key Vaultprivatelink.vaultcore.azure.net
Azure SQLprivatelink.database.windows.net
az network private-dns zone create -g $RG \
  -n privatelink.blob.core.windows.net

az network private-dns link vnet create -g $RG \
  -z privatelink.blob.core.windows.net -n link-hub \
  --virtual-network vnet-hub-prod-wus2-001 --registration-enabled false

Link the same zone to each spoke as well, so that workloads in the spokes resolve private endpoint names without depending on the hub for resolution of their own records. The link is cheap and prevents a class of resolution gaps that are tedious to diagnose.

Step 3: Point on-premises at the resolver, and clear the catch-all

On the on-premises DNS servers, create conditional forwarders for the Azure namespaces that must resolve privately, each pointing at the inbound endpoint IP from Step 1. Forward the recommended public service namespaces that front them, for example blob.core.windows.net, vaultcore.azure.net, and database.windows.net, and let the resolver follow the CNAME into the matching privatelink zone. Do not forward the privatelink zones themselves; forwarding only privatelink is the common mistake that leaves resolution half working. This is a Windows Server DNS or BIND change on your side, not an Azure one.

Now the remediation that discovery exists to enable. If an existing broad conditional forwarder or stub zone for a wider Microsoft domain is present, it can intercept these queries before your specific forwarders are consulted, depending on how your resolver treats overlapping zones, and private endpoints then resolve to public addresses with no error logged anywhere. Remove or narrow that broad forwarder so your specific ones win. This single change is the difference between private endpoints that work and private endpoints that appear to work in every test except the one that matters. Make it before you go further, not after something breaks.

If resolution still misbehaves after clearing the catch-all, the cause is almost always one of a short list, and it is worth walking before you suspect Azure:

  • An authoritative or stub zone on the on-premises servers that shadows the Azure namespace.
  • An overlapping broad forwarder that is more general than the specific one you added.
  • A forwarder pointed at the wrong inbound endpoint IP.
  • A workload virtual network that was never linked to the ruleset.
  • A client that bypasses corporate DNS entirely and asks a public resolver.

Step 4: Resolve on-premises names from Azure, the return path

Everything so far carries queries from on-premises into Azure. The return path, an Azure workload resolving an on-premises name, is a separate mechanism, and it is the half that single-direction builds forget. It needs an outbound endpoint on its own dedicated subnet, a forwarding ruleset that names the on-premises zones and the servers that answer for them, and a link from that ruleset to every workload virtual network that should follow it. The outbound subnet, like the inbound one, is delegated to the resolver, carries nothing else, and must be at least a /28.

# Carve a dedicated outbound subnet if the hub does not already have one (use a free /28)
az network vnet subnet create -g $RG --vnet-name vnet-hub-prod-wus2-001 \
  -n snet-dnsresolver-outbound --address-prefixes 10.40.0.16/28 \
  --delegations Microsoft.Network/dnsResolvers

OUT_SUBNET_ID=$(az network vnet subnet show -g $RG \
  --vnet-name vnet-hub-prod-wus2-001 -n snet-dnsresolver-outbound --query id -o tsv)

az dns-resolver outbound-endpoint create -g $RG \
  --dns-resolver-name dnspr-hub-prod-wus2-001 -n outbound -l westus2 \
  --id "$OUT_SUBNET_ID"

OUT_EP_ID=$(az dns-resolver outbound-endpoint show -g $RG \
  --dns-resolver-name dnspr-hub-prod-wus2-001 -n outbound --query id -o tsv)

az dns-resolver forwarding-ruleset create -g $RG \
  -n fwrs-hub-prod-wus2-001 -l westus2 \
  --outbound-endpoints "[{id:$OUT_EP_ID}]"

# One rule per on-premises zone; the trailing dot on the domain name is required
az dns-resolver forwarding-rule create -g $RG \
  --ruleset-name fwrs-hub-prod-wus2-001 -n corp \
  --domain-name "corp.northforksupply.com." --forwarding-rule-state Enabled \
  --target-dns-servers "[{ip-address:172.16.0.10,port:53},{ip-address:172.16.0.11,port:53}]"

# Link the ruleset to each workload VNet that should resolve on-premises names
PROD_VNET_ID=$(az network vnet show -g $RG -n vnet-prod-wus2-001 --query id -o tsv)

az dns-resolver vnet-link create -g $RG \
  --ruleset-name fwrs-hub-prod-wus2-001 -n link-prod --id "$PROD_VNET_ID"

A ruleset link is a one-to-one relationship with a virtual network and does not depend on peering; a workload virtual network follows the ruleset because it is linked, not because it sits behind the hub. Add a link for every spoke that needs on-premises names, not just the first.

End-to-end test: resolve a real private endpoint from both sides

Everything above is theory until a real private endpoint resolves to a private address from where it needs to. Create a storage account, give it a private endpoint in the Production spoke, and attach it to the blob zone so its record auto-registers. Then resolve the name from a spoke virtual machine and from an on-premises host, and confirm both return a 10.40 address rather than a public one.

SA=stnfdnstest$RANDOM
az storage account create -g $RG -n $SA -l westus2 --sku Standard_LRS

az network private-endpoint create -g $RG -n pe-$SA \
  --vnet-name vnet-prod-wus2-001 --subnet default \
  --private-connection-resource-id $(az storage account show -g $RG -n $SA --query id -o tsv) \
  --group-id blob --connection-name conn-$SA

az network private-endpoint dns-zone-group create -g $RG \
  --endpoint-name pe-$SA -n zg-blob \
  --private-dns-zone privatelink.blob.core.windows.net --zone-name blob

From a virtual machine in the spoke, resolving the storage account’s blob name should now return its private address:

nslookup $SA.blob.core.windows.net
# expect an answer in 10.40.x.x, not a public address

Run the same lookup from an on-premises host. If the conditional forwarder and the catch-all remediation are both correct, it returns the same private address. If on-premises returns a public address while the spoke returns a private one, the broad forwarder is still winning, and you return to Step 3. When both sides agree on the private address, name resolution is correct end to end. Remove the test resources when finished:

az network private-endpoint delete -g $RG -n pe-$SA
az storage account delete -g $RG -n $SA --yes

One more direction to prove, the one Step 4 added: from the same spoke virtual machine, resolve a name that only on-premises knows, and confirm the answer comes back as its on-premises address rather than failing. That exercises the outbound endpoint and the forwarding ruleset end to end.

nslookup fileserver.corp.northforksupply.com
# expect an answer in 172.16.x.x, served by the on-premises DNS the ruleset forwards to

With resolution proven from both the cloud and on-premises side, the network cluster is complete. The hub carries connectivity, the spokes hold workloads, traffic is visible, and names resolve to the right addresses everywhere they are asked. The foundation now has a working network under it, which is the part you were least able to redo later, and it is done.


Azure Landing Zones
‹ Previous: [LZ 4.1] Standing Up the Hub and Connectivity
Next: [LZ 5] The Control Plane You Were Already Standing On