Azure Networking

How to Set Up Azure Private DNS Zones: Auto-Registration, VNet Links and Split-Horizon

You stand up a virtual machine, give it a friendly hostname, and reach for another VM by name — and nothing resolves. Or you create a Private Endpoint for a storage account, everything looks green in the portal, and your app still connects over the public internet because mystorage.blob.core.windows.net resolves to a public IP instead of the private one. Both failures share a root: by default, a virtual network gives your resources only Azure-provided DNS (the resolver at 168.63.129.16), which resolves public names and the auto-generated internal.cloudapp.net suffix — but it will not host your private records, and it has no idea your Private Endpoint exists. Azure Private DNS Zones are the missing piece: a DNS zone (e.g. corp.kloudvin.com or privatelink.blob.core.windows.net) that lives entirely inside Azure, is never published publicly, and answers queries only for the virtual networks you explicitly link to it.

This article is a build guide. By the end you will have created a private zone, linked it to one or more VNets, switched on auto-registration so VMs register their own A records the moment they boot, and stood up split-horizon (split-brain) DNS so app.kloudvin.com returns a private 10.x address inside the VNet and a public address to the internet. You will also wire up the special privatelink.* zones that make Private Endpoints resolve correctly — the single most common reason teams adopt Private DNS. Everything is shown three ways: the portal click-path, the az CLI command, and a reusable Bicep module, with expected output and a validation at each step.

The mental shift: a private DNS zone is not attached to a VNet the way a subnet is. It is a standalone resource you create once, then associate with VNets through virtual network links. Resolution is decided per-VNet by which zones it is linked to and whether that link has registration enabled. Get that right and the rest — auto-registration, split-horizon, privatelink zones, hub-and-spoke — falls out of it.

What problem this solves

Without private DNS, name resolution inside Azure is crippled in three specific ways, and each one shows up as a different production incident.

You cannot resolve your own machines by a name you control. Azure-provided DNS resolves a VM’s auto-assigned hostname within the same VNet via the internal.cloudapp.net suffix — but only for VMs in that exact VNet, and with a suffix you didn’t choose. The moment you peer two VNets, or want a stable name like db01.corp.kloudvin.com that survives a rebuild with a new IP, the built-in resolver cannot help. Teams paper over this with hosts files, hard-coded IPs, or a self-managed pair of DNS server VMs running BIND or Windows DNS — operational debt you maintain forever.

Private Endpoints resolve to public IPs, so traffic skips the private path entirely. The quiet, dangerous one. A Private Endpoint gives a PaaS resource (storage, SQL, Key Vault) a private IP in your VNet, but the resource’s public FQDN — mydb.database.windows.net — still resolves, by public DNS, to a public IP. Your app keeps using the public FQDN, gets the public IP, and connects over the internet (or fails, if you’ve locked the public path down). The endpoint sits unused while you pay for it. The fix is a privatelink.* private zone that overrides that FQDN to the private IP — and it must be linked to the consumer’s VNet, not just the one holding the endpoint.

The same name has to mean different things inside and outside. A web app at app.kloudvin.com may be reachable publicly through Front Door for customers, while internal services reach it directly on a private IP — same hostname, two answers. Without split-horizon you end up with two names (app.kloudvin.com and app-internal.kloudvin.com), config drift, and certificates that don’t match. A private view of kloudvin.com shadows the public one only for linked VNets, so one name resolves correctly from both sides.

Who hits this: anyone running VMs that talk by name; every team that adopts Private Endpoints; and any workload with a hybrid or multi-environment naming requirement. The cost ranges from “annoying, falls back to IPs” to “we thought traffic was private for six months and it wasn’t.”

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should understand the basics of an Azure virtual network (VNet) and subnets — if not, start with Azure Virtual Network, Subnets and NSGs: Networking Fundamentals and Azure VNet IP Address Planning: CIDR, Subnetting and Avoiding Overlap Before You Deploy. Know how DNS works at a high level (zones, records, recursion, TTL), be comfortable running az in Cloud Shell, and able to read JSON. Knowing what a Private Endpoint is helps, since the marquee use case is making them resolve; Azure Private Endpoint vs Service Endpoint: Secure PaaS Access is the primer.

This sits in the Networking track, the name-resolution layer everything above depends on. It is upstream of Azure Private Link and Private DNS: Keeping PaaS Off the Public Internet (this article is the focused build guide for the DNS half), and the private mirror of How to Host Public DNS on Azure DNS: Zone Delegation, Alias Records and Apex Domains — same product family, opposite audience. In hub-and-spoke it pairs with Azure VNet Peering Done Right: Gateway Transit, Global Peering and Transitivity Limits, because peering moves traffic but not DNS — you still link zones to each VNet explicitly.

Who owns which layer, so you call the right person during an incident:

Layer What lives here Who usually owns it Failure it causes
Public DNS (registrar / Azure DNS public) Internet records, delegation DNS / platform team External name fails; split-horizon public side wrong
Azure resolver (168.63.129.16) Default VNet DNS, recursion Microsoft (platform) Bypassed if custom DNS is set
Private DNS zone + VNet links Private records, auto-registration Network / platform team Resolves public not private; PE unusable
Custom DNS servers (if set) Conditional forwarding, on-prem zones Network team Private zone bypassed; must forward to 168.63.129.16
The resource (VM / PE) The actual IP resolved to App / platform team Record points at a dead/old IP

Core concepts

Six ideas make every later step obvious; the tables and lab assume them.

A private zone is a standalone resource, linked — not attached — to VNets. You create a Microsoft.Network/privateDnsZones resource whose name is the DNS suffix (e.g. corp.kloudvin.com). It does nothing until you add virtual network links (.../virtualNetworkLinks), each pointing at a VNet, and a VNet resolves a zone only if linked to it. A zone serves up to 1,000 VNet links and a VNet can link to up to 1,000 private zones — so one central zone can serve an entire estate.

The Azure-provided resolver does the choosing, for free, on “default” DNS. Every VNet has a virtual DNS server at the fixed 168.63.129.16. On a query it checks the linked private zones first; on a match (longest suffix) it answers privately, else recurses to public DNS. You don’t point clients at the zone — you leave the VNet’s DNS on “Default (Azure-provided)” and the resolver consults linked zones automatically. The crucial dependency: switch to custom DNS servers and you lose automatic private-zone resolution unless they forward back to 168.63.129.16.

Auto-registration is a per-link switch, and a VNet may have at most one registration link. A link has a registration enabled flag. When true, every VM in that VNet automatically gets an A record (<vmhostname>.<zone>) created at boot, updated if its IP changes, and removed on delete. The hard rule: a VNet can be the registration VNet for only one private zone — it can be resolution-only-linked to many, but registration belongs to exactly one. So you pick one zone (typically corp.kloudvin.com) as the registration target and treat the rest as resolution-only.

Resolution-only vs registration. A link with registration disabled is resolution-only: resources resolve the zone’s records but VMs don’t auto-register. The privatelink.* zones are always resolution-only (PaaS endpoints register via the Private Endpoint, not the VM mechanism). Most links across an estate are resolution-only; the single registration link is the exception.

Split-horizon is just “create a private zone with the same name as a public one.” If you own the public zone kloudvin.com and also create a private kloudvin.com linked to your VNets, linked VNets see the private records (private IPs) while the internet sees the public records. The two zones are independent and Azure never publishes the private one. Put a private A for app.kloudvin.com → 10.20.1.10 in the private zone and a public A/alias for the same name in the public zone; each audience gets its own answer.

privatelink.* zones are the special case that makes Private Endpoints work. Microsoft reserves names of the form privatelink.<service>.<suffix> (e.g. privatelink.blob.core.windows.net). When you create a Private Endpoint, the resource’s public FQDN becomes a CNAME to a *.privatelink.* name, and a matching private zone holds the A record mapping it to the endpoint’s private IP. Link that zone to every VNet that needs the private path and the public FQDN resolves privately. Get the name exactly right (table later) or it silently won’t work.

The vocabulary in one table

Pin down every moving part before the deep sections (the glossary repeats them for lookup):

Concept One-line definition Where it lives Why it matters
Private DNS zone A DNS zone hosted in Azure, never public RG (global resource) Holds your private records
Virtual network link Association of a zone to one VNet Child of the zone A VNet resolves a zone only if linked
Registration Per-link flag: VMs auto-create A records On the link One registration link per VNet
Resolution-only link Link with registration off On the link Resolves but doesn’t auto-register
Azure resolver The 168.63.129.16 virtual DNS server Every VNet Chooses private zone vs public, free
Auto-registration VM A records created/updated/removed The registration link Zero-touch records for VMs
Split-horizon Same name, private inside, public outside Two same-named zones One hostname, two audiences
privatelink.* zone Reserved zone making a PE resolve Resolution-only link The #1 reason teams adopt private DNS
A / CNAME / SOA / SRV / TXT Record types a zone holds Record sets The name→value mappings
SOA record Auto-created zone authority record Created with the zone You don’t manage it

How resolution actually decides: the private zone vs public DNS

Almost every “it resolved to the wrong IP” bug traces to the resolver’s decision. A resource queries 168.63.129.16; if the name suffix-matches a private zone linked to this VNet (longest match wins) and the record exists, it answers privately — otherwise it recurses to public DNS. Two consequences are the whole game:

Situation What resolves Why
Name in a private zone linked to the VNet Private record Resolver checks linked zones first
Name in a private zone not linked to the VNet Public (or NXDOMAIN) Unlinked zone is invisible to that VNet
Name in no private zone Public answer Recurses to public DNS
VNet on custom DNS servers (no forward) Whatever they say — zone ignored Query never reaches 168.63.129.16
Custom DNS that forwards to 168.63.129.16 Private record (back in play) Forwarder restores zone consultation

The custom-DNS row is the most common operational trap: set custom DNS servers on a VNet (for on-prem/AD) and overnight every Private Endpoint and private record stops resolving, because the query never reaches the Azure resolver. We return to the fixes in troubleshooting.

Creating the zone and your first VNet link

Everything starts with a zone and one link. We use corp.kloudvin.com (your internal namespace) throughout; substitute your own.

The portal click-path

  1. Create a resource → search Private DNS zoneCreate.
  2. Resource group: rg-dns-lab. Name: corp.kloudvin.com — the name is the DNS suffix (must be a valid domain; Azure does not verify ownership for private zones, so control it to avoid clashes). Review + createCreate.
  3. Open the zone — there’s no link yet, just an empty record list and an auto-created SOA. Go to Virtual network linksAdd. Name it (link-hub), pick the virtual network, and tick Enable auto registration for your one registration VNet (leave it unticked for resolution-only). Create.

That’s the minimum: a zone plus one link. Resources in the linked VNet can now resolve any record you add.

The az CLI

# Variables
RG=rg-dns-lab
LOC=centralindia
ZONE=corp.kloudvin.com
VNET=vnet-hub

# 1. Resource group
az group create -n $RG -l $LOC -o table

# 2. (A VNet to link to — skip if you already have one)
az network vnet create -g $RG -n $VNET --address-prefixes 10.20.0.0/16 \
  --subnet-name snet-workload --subnet-prefixes 10.20.1.0/24 -o table

# 3. Create the private zone (zones are global; location is 'global' implicitly)
az network private-dns zone create -g $RG -n $ZONE -o table

Expected: the zone returns "name": "corp.kloudvin.com" and "numberOfRecordSets": 1 (the SOA). Now link it, with registration ON:

# 4. Link the zone to the VNet WITH auto-registration enabled
az network private-dns link vnet create -g $RG -n link-hub \
  --zone-name $ZONE --virtual-network $VNET --registration-enabled true -o table

Expected: a link with "registrationEnabled": true and "virtualNetworkLinkState": "Completed". The --registration-enabled flag is the difference between an auto-registration link and a resolution-only one (false).

The Bicep module

The reusable unit you copy across environments. Note the link is a child resource of the zone:

@description('Private DNS zone name = the DNS suffix, e.g. corp.kloudvin.com')
param zoneName string = 'corp.kloudvin.com'

@description('Resource ID of the VNet to link with auto-registration')
param registrationVnetId string

// Private DNS zones are GLOBAL resources — location is literally 'global'
resource zone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
  name: zoneName
  location: 'global'
}

// The registration link (one per VNet, max)
resource link 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
  parent: zone
  name: 'link-hub'
  location: 'global'
  properties: {
    registrationEnabled: true       // <-- auto-registration ON
    virtualNetwork: {
      id: registrationVnetId
    }
  }
}

output zoneId string = zone.id

Deploy with az deployment group create -g rg-dns-lab --template-file zone.bicep --parameters registrationVnetId=<vnet-resource-id>. The location: 'global' line trips up first-timers — private zones are not regional, and a regional location fails validation.

Confirm it works

From a VM in the linked VNet, add a test record and resolve it:

# Add a test A record, then resolve it from a VM in vnet-hub
az network private-dns record-set a add-record -g $RG --zone-name $ZONE \
  --record-set-name test --ipv4-address 10.20.1.250
# On a VM inside vnet-hub:
nslookup test.corp.kloudvin.com     # -> 10.20.1.250 (private), via 168.63.129.16

If nslookup returns 10.20.1.250, the zone, link, and resolver all work. NXDOMAIN means the VM’s VNet isn’t linked, or it’s on custom DNS — both covered in troubleshooting.

Auto-registration: zero-touch records for your VMs

Auto-registration turns a private zone from a manual record list into a living inventory of your VMs. With it enabled on a link, the platform manages this lifecycle with no action required:

VM event What happens in the zone Record name Timing
VM created in the registration VNet A record created <vm-hostname>.<zone> ~seconds after the NIC gets its IP
VM private IP changes (dealloc/realloc) A record updated to new IP same On next IP assignment
VM stopped (deallocated) A record may change/remove same Depends on IP retention
VM deleted A record removed same Shortly after deletion
Hostname changed in the OS New record may appear new hostname After the OS reports it

The record name is the VM’s hostname, not its Azure resource name — usually the same but they can diverge. The zone becomes self-maintaining: nslookup web01.corp.kloudvin.com works the instant web01 boots and vanishes when you delete the VM, so the auto-registered set never accumulates stale entries.

The constraints you must respect:

Constraint Detail Consequence if violated
One registration link per VNet A VNet auto-registers into exactly one zone Second registration link fails to create
Per-link, per-VNet Only VMs in the registration VNet register Peered-VNet VMs do not register here
Only A records, only VMs PaaS / LBs / on-prem don’t auto-register Add those manually
Hostname, not resource name Record = OS hostname Mismatch confuses lookups
No reverse (PTR) Forward A only Reverse lookups need a manual PTR zone

Because auto-registration is scoped to the VNet owning the registration link, a hub-and-spoke estate must choose: give each VNet its own registration zone (like spoke1.corp.kloudvin.com), or let one VNet auto-register and add manual records for the rest. There is no “auto-register everywhere” switch — the one-link-per-VNet rule forbids it.

Toggle registration on an existing link:

# Flip an existing link to resolution-only (registration off)
az network private-dns link vnet update -g $RG -n link-hub \
  --zone-name $ZONE --registration-enabled false -o table

Manual records: A, CNAME, SRV, TXT and the PaaS endpoints

Auto-registration covers your VMs; everything else you add by hand. A private zone supports the standard record set, and the az network private-dns record-set verbs are consistent: <type> add-record appends a value, <type> create makes an empty set with a custom TTL, <type> update changes the TTL.

Record type Use it for Example value Notes
A Name → IPv4 10.20.1.10 Most common; PE records are A
AAAA Name → IPv6 fd00::10 Only if you run IPv6
CNAME Alias to another name web01.corp.kloudvin.com Cannot exist at the apex
SRV Service location (port + target) 0 5 443 web01... AD-style service discovery
TXT Arbitrary text "owner=team-a" Metadata, verification
MX Mail exchanger 10 mail.corp... Rare in a private zone
PTR Reverse (IP → name) in in-addr.arpa Needs a separate reverse zone
SOA Zone authority (auto) You don’t create/delete it

Add a manual A record for a load-balanced front end:

# A record for an internal load balancer front end
az network private-dns record-set a add-record -g $RG --zone-name $ZONE \
  --record-set-name api --ipv4-address 10.20.1.100

# CNAME so legacy.corp.kloudvin.com points at api.corp.kloudvin.com
az network private-dns record-set cname set-record -g $RG --zone-name $ZONE \
  --record-set-name legacy --cname api.corp.kloudvin.com

# Set a custom TTL (default is 3600s) on a record set you create explicitly
az network private-dns record-set a create -g $RG --zone-name $ZONE \
  --name shortlived --ttl 60
az network private-dns record-set a add-record -g $RG --zone-name $ZONE \
  --record-set-name shortlived --ipv4-address 10.20.1.77

The same in Bicep, each record set a child of the zone:

resource aApi 'Microsoft.Network/privateDnsZones/A@2024-06-01' = {
  parent: zone
  name: 'api'                  // becomes api.corp.kloudvin.com
  properties: {
    ttl: 3600
    aRecords: [
      { ipv4Address: '10.20.1.100' }
    ]
  }
}

resource cnameLegacy 'Microsoft.Network/privateDnsZones/CNAME@2024-06-01' = {
  parent: zone
  name: 'legacy'
  properties: {
    ttl: 3600
    cnameRecord: { cname: 'api.corp.kloudvin.com' }
  }
}

One apex rule (same as public DNS): you cannot put a CNAME at the zone apex (@) — the apex must hold the SOA. To resolve the bare corp.kloudvin.com, use an A record at @, not a CNAME.

Wiring Private Endpoints: the privatelink.* zones

This is the payoff section — the reason most teams are here. A Private Endpoint gives a PaaS resource a private NIC and IP, but resolution of the resource’s public FQDN must be redirected to that private IP — the job of a privatelink.* private zone.

How it chains together, for storage blob: the endpoint gets a private IP (say 10.20.2.5), and Azure rewrites the public FQDN mystorage.blob.core.windows.net into a CNAME to mystorage.privatelink.blob.core.windows.net. You create a private zone named exactly privatelink.blob.core.windows.net with an A record mystorage → 10.20.2.5 (the “integrate with private DNS” toggle does this) and link it resolution-only to every consuming VNet. Now a query from a linked VNet follows the CNAME, finds the A record, and returns 10.20.2.5; from the internet the same name returns a public IP, because the zone isn’t linked there.

The zone name must match the service exactly. A wrong suffix (privatelink.blob.windows.net instead of privatelink.blob.core.windows.net) leaves the CNAME target with no matching zone and resolution falls through to public — the endpoint looks configured but does nothing. The common ones:

Azure service (sub-resource) privatelink zone name
Storage — Blob privatelink.blob.core.windows.net
Storage — File privatelink.file.core.windows.net
Storage — Queue/Table/DFS privatelink.queue/.table/.dfs.core.windows.net
Azure SQL Database privatelink.database.windows.net
Cosmos DB (SQL API) privatelink.documents.azure.com
Key Vault privatelink.vaultcore.azure.net
Container Registry privatelink.azurecr.io
App Service / Functions privatelink.azurewebsites.net
Service Bus / Event Hubs privatelink.servicebus.windows.net
Azure Monitor (AMPLS) privatelink.monitor.azure.com (+ more)

Create the zone, link it, and let the endpoint populate the record:

PLZONE=privatelink.blob.core.windows.net

# 1. Create the privatelink zone (resolution-only everywhere)
az network private-dns zone create -g $RG -n $PLZONE -o table

# 2. Link it to the consuming VNet (registration OFF — PEs don't auto-register via VM mechanism)
az network private-dns link vnet create -g $RG -n link-blob-hub \
  --zone-name $PLZONE --virtual-network $VNET --registration-enabled false -o table

# 3. When you create the Private Endpoint, attach a private-dns-zone-group so Azure
#    writes the A record automatically into the linked zone:
az network private-endpoint dns-zone-group create -g $RG \
  --endpoint-name pe-blob --name pe-blob-zg \
  --private-dns-zone $PLZONE --zone-name blob

The DNS zone group (dns-zone-group) makes the A record self-maintaining: it ties the endpoint to the zone so the A record is created (and updated if the endpoint IP changes) without you typing IPs. Always prefer it over hand-adding the record — a manual record goes stale if the endpoint is recreated.

The recurring mistake — worth stating loudly — is forgetting to link the privatelink zone to the consumer’s VNet. If your app runs in vnet-spoke-app but the zone is only linked to vnet-hub where the endpoint lives, the app resolves the public IP. Link the zone wherever the clients are, and automate it at scale with Azure Policy (“deploy private DNS zone group on Private Endpoint create”).

Split-horizon: one name, two answers

Split-horizon (split-brain) DNS means the same FQDN returns different addresses depending on who asks. You create a private zone whose name matches a public zone you own, then link it only to the VNets that should see the private view.

For kloudvin.com:

Audience Zone consulted app.kloudvin.com resolves to How
Internet user / customer Public kloudvin.com Public IP / Front Door Normal public recursion
Resource in a linked VNet Private kloudvin.com 10.20.1.10 (internal) Resolver checks linked zone first
Resource in an unlinked VNet Public kloudvin.com Public IP / Front Door No linked zone → recurse

The two zones are independent and Azure never publishes the private one. You decide per record what each side sees, and can override only the handful of names that need an internal address.

# Public side already exists (Azure DNS public zone or your registrar).
# Create the PRIVATE shadow zone with the SAME name:
az network private-dns zone create -g $RG -n kloudvin.com -o table

# Link it to the internal VNets only
az network private-dns link vnet create -g $RG -n link-internal \
  --zone-name kloudvin.com --virtual-network $VNET --registration-enabled false -o table

# Private answer for app.kloudvin.com inside the VNet
az network private-dns record-set a add-record -g $RG --zone-name kloudvin.com \
  --record-set-name app --ipv4-address 10.20.1.10

Now from inside vnet-hub, nslookup app.kloudvin.com returns 10.20.1.10; from your laptop it returns whatever the public zone says. The win is one hostname and one certificate valid from both sides — no app-internal.kloudvin.com drift, no environment-specific config. The caveat: the private zone shadows the public one for linked VNets, completely hiding the public record there — keep overlapping records intentionally in sync.

Architecture at a glance

The diagram traces a single DNS query as it really flows and shows where each failure class bites. Read it left to right. A client VM in a linked virtual network asks for a name and sends the query to the Azure-provided resolver at 168.63.129.16 — it never talks to the zone directly. The resolver consults the private DNS zones linked to that VNet: the auto-registration zone corp.kloudvin.com (which holds VM A records that appeared by themselves), the privatelink.blob.core.windows.net zone (which makes a Private Endpoint resolve to its 10.20.2.x private IP), and the split-horizon kloudvin.com shadow zone. If a linked zone holds the longest-suffix match, the resolver answers privately; if not, it recurses to public DNS and returns the public address — which is exactly what an unlinked VNet or an internet client gets.

Follow the numbered badges for the failure map. The query path breaks at the VNet link if the consumer VNet isn’t linked (the storage endpoint then resolves public), at the registration link if you try to add a second one (only one per VNet), and at the resolver hop if the VNet was switched to custom DNS servers that don’t forward to 168.63.129.16 — the single most common “everything stopped resolving” outage. The legend narrates each number as symptom, the exact command to confirm, and the fix. The whole method is: a resource queries the resolver, the resolver prefers linked private zones over public, and every bug is either a missing link, a wrong zone name, or a resolver that was bypassed.

Azure Private DNS resolution flow: a client VM in a linked virtual network sends a DNS query to the Azure-provided resolver at 168.63.129.16, which consults the private DNS zones linked to that VNet — an auto-registration zone corp.kloudvin.com holding VM A records, a privatelink.blob.core.windows.net zone mapping a storage Private Endpoint to its private IP, and a split-horizon kloudvin.com shadow zone — answering privately on a linked-zone match or recursing to public DNS otherwise; numbered badges mark the failure points: consumer VNet not linked (endpoint resolves public), a second registration link rejected, a stale or wrong-suffix privatelink zone, and a VNet switched to custom DNS that bypasses the resolver

Real-world scenario

Meridian Logistics runs a fleet-tracking platform on Azure in a hub-and-spoke topology in Central India: a hub vnet-hub (10.20.0.0/16) with shared services, and three spokes — vnet-app (10.21.0.0/16), vnet-data (10.22.0.0/16), and vnet-mgmt (10.23.0.0/16) — all peered to the hub. The five-engineer platform team had adopted Private Endpoints for Azure SQL and blob storage, ticking “integrate with a private DNS zone” during creation each time. DNS spend was effectively nil.

The incident surfaced as intermittent slowness in the tracking app plus occasional outright failures writing to blob storage — but only from the app tier, never from the management jump box. The on-call engineer’s first move was to check the endpoints: all healthy, private IPs assigned, “approved.” The storage firewall denied public access. So how was the app reaching storage at all?

The breakthrough came from running nslookup meridianfleet.blob.core.windows.net from an app-tier VM versus the jump box. From the jump box (vnet-mgmt) it returned the private 10.20.2.5; from the app VM (vnet-app) it returned a public IP. The cause: when each endpoint was created, the wizard created the privatelink.blob.core.windows.net and privatelink.database.windows.net zones and linked them — but only to the VNet the wizard defaulted to (the hub, and for one endpoint vnet-mgmt). The spokes vnet-app and vnet-data were never linked. So app-tier resolution fell through to public DNS, returned the public IP, and traffic tried the public path — which the firewall sometimes blocked (the failures) and sometimes allowed via a stale cache (the slowness).

The fix was small, the lesson large. They linked both privatelink.* zones (resolution-only) to every spoke:

for VN in vnet-app vnet-data; do
  for Z in privatelink.blob.core.windows.net privatelink.database.windows.net; do
    az network private-dns link vnet create -g rg-net -n "link-$VN-${Z%%.*}" \
      --zone-name $Z --virtual-network $VN --registration-enabled false
  done
done

Within a minute (the affected records’ TTL was 30s, though cached public answers lingered until theirs expired), app-tier nslookup returned 10.20.2.5 and blob writes went private and stopped failing. To stop future endpoints shipping without DNS wiring, they applied the built-in Azure Policy that deploys a private DNS zone group on endpoint creation, centralised all privatelink.* zones in the hub, and wrote a one-line runbook: “After any Private Endpoint, confirm the privatelink zone is linked to the consumer VNet, not just the endpoint’s VNet.” Cost impact was zero; the architecture was correct except for four missing links.

The incident as a timeline, because the order of moves is the lesson:

Time Symptom Action taken Effect What it should have been
T+0 App-tier blob writes slow + failing Check Private Endpoints All healthy — confusing Ask what IP the app resolves
T+8m Still failing Check storage firewall Public denied — so how connecting? Clue: it uses the public FQDN
T+15m Root cause hunt nslookup from app VM vs jump box App gets public, jump box private The breakthrough
T+20m Cause found Inspect privatelink zone links Spokes never linked
T+25m Mitigated Link zones to all spokes App resolves private; failures clear Correct fix
+1 day Hardened Azure Policy + centralise zones No future endpoint ships without DNS The durable fix is policy

Advantages and disadvantages

Private DNS Zones are a managed, no-server way to run private name resolution — mostly upside, with a few sharp edges to design around.

Advantages (why this model helps you) Disadvantages (why it bites)
Fully managed: no DNS server VMs to patch, scale, or pair for HA Resolution is per-VNet via links — forget the link and it silently resolves public
Auto-registration gives VMs A records with zero effort, self-cleaning Auto-registration is one zone per VNet — no single central registration zone
privatelink.* zones make Private Endpoints “just work” with a zone group Wrong privatelink suffix fails silently — endpoint looks fine, resolves public
Split-horizon is trivial: a same-named private zone, no extra product A VNet on custom DNS bypasses the resolver unless it forwards to 168.63.129.16
Free for the zone; only tiny per-query/record charges At hub-spoke scale you link every zone to every VNet — verbose without automation
Works with the free Azure resolver — no extra infra for VNet-only resolution On-prem ↔ Azure resolution needs an Azure DNS Private Resolver (extra)
Records via portal/CLI/Bicep/Policy — fully IaC and governable No DNSSEC or zone transfer (AXFR); not a drop-in for every legacy DNS need

The model is right for VNet-internal resolution, Private Endpoint integration, and split-horizon — the vast majority of Azure workloads. It bites when resolution must cross on-prem ↔ Azure (add a Private Resolver), when custom DNS is already in play (add conditional forwarding), or at many-VNet scale (add Policy and a naming convention). All three are manageable; none is a reason to run your own DNS fleet.

Hands-on lab

The centerpiece: build the whole thing end to end — a zone, an auto-registration link, two self-registering VMs, a manual record, and a privatelink zone for a storage Private Endpoint — then validate and tear it down. Free-tier-friendly except for two B1s VMs and one storage account (a few rupees for the hour). Run in Cloud Shell (Bash).

Prerequisites: an Azure subscription, Contributor on a resource group (or the subscription), and az ≥ 2.55. Everything below creates resources in one resource group you delete at the end.

Step 1 — Variables and resource group.

RG=rg-dns-lab
LOC=centralindia
ZONE=corp.kloudvin.com
PLZONE=privatelink.blob.core.windows.net
VNET=vnet-lab
SA=sadnslab$RANDOM          # globally-unique storage name
az group create -n $RG -l $LOC -o table

Expected: a resource-group row with "provisioningState": "Succeeded".

Step 2 — Create a VNet with a workload subnet and a PE subnet.

az network vnet create -g $RG -n $VNET --address-prefixes 10.30.0.0/16 \
  --subnet-name snet-workload --subnet-prefixes 10.30.1.0/24 -o table
az network vnet subnet create -g $RG --vnet-name $VNET -n snet-pe \
  --address-prefixes 10.30.2.0/24 -o table

Expected: the VNet and two subnets. Leave it on default DNS — custom DNS servers would stop the private zone being consulted.

Step 3 — Create the private zone and link it WITH auto-registration.

az network private-dns zone create -g $RG -n $ZONE -o table
az network private-dns link vnet create -g $RG -n link-lab \
  --zone-name $ZONE --virtual-network $VNET --registration-enabled true -o table

Expected: the zone shows numberOfRecordSets: 1 (SOA); the link registrationEnabled: true, virtualNetworkLinkState: Completed.

Step 4 — Create two VMs in the workload subnet (they will auto-register).

for VM in web01 web02; do
  az vm create -g $RG -n $VM --image Ubuntu2204 --size Standard_B1s \
    --vnet-name $VNET --subnet snet-workload \
    --admin-username azureuser --generate-ssh-keys --public-ip-address "" \
    --no-wait
done
# wait for both to finish
az vm wait -g $RG -n web01 --created
az vm wait -g $RG -n web02 --created

Expected: both VMs reach Succeeded. --public-ip-address "" (no public IP) is deliberate — this is a private-DNS lab.

Step 5 — Watch auto-registration populate the zone. No DNS commands run — records appear by themselves:

az network private-dns record-set a list -g $RG --zone-name $ZONE \
  --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table

Expected: web01 and web02 listed with 10.30.1.x addresses you never typed — the zone is now a live inventory.

Step 6 — Validate VM-to-VM resolution by name. Run a command on web01 remotely:

az vm run-command invoke -g $RG -n web01 --command-id RunShellScript \
  --scripts "nslookup web02.corp.kloudvin.com"

Expected: web02.corp.kloudvin.com resolves to its 10.30.1.x private IP, served by 168.63.129.16 — no hosts-file editing.

Step 7 — Add a manual record (for something that doesn’t auto-register).

az network private-dns record-set a add-record -g $RG --zone-name $ZONE \
  --record-set-name api --ipv4-address 10.30.1.100
az vm run-command invoke -g $RG -n web01 --command-id RunShellScript \
  --scripts "nslookup api.corp.kloudvin.com"

Expected: api.corp.kloudvin.com → 10.30.1.100. Manual and auto-registered records coexist in the same zone.

Step 8 — Stand up a storage account + Private Endpoint + privatelink zone.

# Storage account (disable public access to force the private path)
az storage account create -g $RG -n $SA --sku Standard_LRS \
  --kind StorageV2 --public-network-access Disabled -o table

# The privatelink zone, linked resolution-only
az network private-dns zone create -g $RG -n $PLZONE -o table
az network private-dns link vnet create -g $RG -n link-pl-blob \
  --zone-name $PLZONE --virtual-network $VNET --registration-enabled false -o table

# Create the Private Endpoint for the blob sub-resource
SAID=$(az storage account show -g $RG -n $SA --query id -o tsv)
az network private-endpoint create -g $RG -n pe-blob \
  --vnet-name $VNET --subnet snet-pe \
  --private-connection-resource-id $SAID --group-id blob \
  --connection-name pe-blob-conn -o table

# Tie the endpoint to the zone so the A record is written automatically
az network private-endpoint dns-zone-group create -g $RG \
  --endpoint-name pe-blob --name pe-blob-zg \
  --private-dns-zone $PLZONE --zone-name blob -o table

Expected: the endpoint is created and the DNS zone group writes an A record into privatelink.blob.core.windows.net for the account’s private IP (a 10.30.2.x address).

Step 9 — Validate the Private Endpoint resolves privately.

az vm run-command invoke -g $RG -n web01 --command-id RunShellScript \
  --scripts "nslookup $SA.blob.core.windows.net"

Expected: the lookup follows the CNAME to $SA.privatelink.blob.core.windows.net and returns the private 10.30.2.x IP, not a public one. A public IP would mean the privatelink zone isn’t linked to this VNet — the exact failure from the real-world scenario.

Validation checklist. You created a zone, auto-registered two VMs without touching DNS, resolved VM-to-VM by name, added a manual record, and made a Private Endpoint resolve privately via a privatelink zone and zone group. The steps mapped to what each proves:

Step What you did What it proves
3 Zone + registration link The link activates resolution & registration
5 Records appeared unprompted Auto-registration is zero-touch
6 nslookup web02... from web01 VM-by-name works via 168.63.129.16
7 Manual api record Manual and auto records coexist
8–9 PE + privatelink zone + zone group Makes Private Endpoints resolve

Cleanup (avoid lingering VM/storage charges).

az group delete -n $RG --yes --no-wait

Cost note. Two B1s VMs plus one Standard_LRS account for an hour is well under ₹60; the zones and links are effectively free. Deleting the resource group stops everything, and because the VMs auto-registered, their A records vanish with them.

Common mistakes & troubleshooting

Failures cluster tightly: a missing link, a wrong zone name, a bypassed resolver, or stale state. The playbook — scan the table mid-incident, then the key entries with exact confirm commands underneath.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 PE resolves to a public IP from the app privatelink zone not linked to the consumer VNet nslookup from app VM; link vnet list --zone-name <plzone> Link the zone (registration off) to that VNet
2 Private records don’t resolve (NXDOMAIN/public) VNet uses custom DNS servers, bypassing 168.63.129.16 vnet show --query dhcpOptions.dnsServers (non-empty) Default DNS, or forward zone names to 168.63.129.16
3 Second registration link fails to create One registration link per VNet already exists link vnet list (find registrationEnabled:true) Disable the other, or use a different zone/VNet
4 VM didn’t auto-register VM not in the registration VNet, or link resolution-only Check the link’s registrationEnabled + the VM’s VNet Enable registration on the right VNet’s link
5 PE works in hub, not in spoke privatelink zone linked only to hub List zone links; compare to spoke VNet Link the zone to the spoke too (peering ≠ DNS)
6 Wrong/old IP returned for a name Stale record or cached answer (TTL) record-set a show ...; check TTL Update the record; lower TTL before changes
7 privatelink zone created but PE still public Wrong zone name (suffix off by a label) Compare zone name to the service’s exact suffix Recreate with the exact privatelink.<service> name
8 Resolves from VM, fails from on-prem On-prem can’t reach 168.63.129.16 (VNet-internal) Query the private name from on-prem → fails Add DNS Private Resolver inbound endpoint + forwarding
9 Apex CNAME rejected CNAME illegal at @ (must hold SOA) Error: “CNAME record sets cannot be created at the zone apex” Use an A record at @, not CNAME
10 Records gone after VM stop/start IP changed; auto-registration re-registered record-set a list shows the new IP Use a static private IP or a manual record

#1 / #5 — The Private Endpoint resolves public (the most common bug)

Run nslookup from the actual consuming VM, not Cloud Shell (which isn’t in your VNet):

az vm run-command invoke -g $RG -n <app-vm> --command-id RunShellScript \
  --scripts "nslookup <account>.blob.core.windows.net"
# Public IP returned  -> the privatelink zone is not linked to THIS VNet
az network private-dns link vnet list -g $RG --zone-name privatelink.blob.core.windows.net \
  --query "[].{link:name, vnet:virtualNetwork.id, reg:registrationEnabled}" -o table

If the consumer VNet isn’t listed, link it (resolution-only). Peering does not carry DNS — every consuming VNet needs its own link.

#2 — Custom DNS servers bypass the private zone

The silent killer in established networks. Check for custom DNS:

az network vnet show -g $RG -n <vnet> --query "dhcpOptions.dnsServers" -o tsv

If that returns IPs (your AD/DNS servers) rather than empty, those servers handle resolution and never consult the private zone. Fixes: revert to default DNS (az network vnet update --dns-servers ''); conditionally forward the private + privatelink.* names to 168.63.129.16; or front them with an Azure DNS Private Resolver. The forwarder approach is standard when you must keep custom DNS for on-prem AD.

#3 / #4 — Auto-registration won’t behave

If you see… It’s probably… Do this
“registration link already exists” This VNet already auto-registers into another zone One registration zone per VNet; resolution-only for the rest
VM exists but no A record VNet’s link is resolution-only (registration off) Set --registration-enabled true on that link
Record name ≠ Azure VM name Record uses the OS hostname Align hostname, or add a manual record
Peered-VNet VMs don’t register Registration is per-VNet, not transitive Give that VNet its own registration zone

#8 — On-prem can’t resolve Azure private names

168.63.129.16 is reachable only from inside a VNet — on-prem hosts (over VPN/ExpressRoute) cannot query it directly. To let on-prem resolve corp.kloudvin.com or a privatelink name, deploy an Azure DNS Private Resolver with an inbound endpoint (a private IP your on-prem DNS forwards to), plus an outbound endpoint + forwarding ruleset if Azure needs on-prem zones. This replaces the old DNS-forwarder-VM pattern.

Best practices

Security notes

Private DNS is a security control as much as a convenience — in the Private Endpoint flow its whole job is keeping traffic off the public internet — so treat its config as security-relevant.

Concern Guidance
Keep PaaS traffic private The privatelink zone is what makes a Private Endpoint actually used; an unlinked zone silently takes the public path. Verify with nslookup from real consumers and pair zones with public access disabled (--public-network-access Disabled).
RBAC on zones and links Scope Private DNS Zone Contributor tightly. A malicious or mistaken A record in a linked zone can hijack a name (e.g. api.corp → attacker IP) for every VM in the VNet.
No accidental public exposure Private zones are never published — but a public zone of the same name (split-horizon) is. Put internal-only names in the private zone, not the public one.
Don’t leak internal topology Private zone contents (hostnames, IP plan) are recon material; restrict read access and don’t mirror private records into the public zone.
Custom DNS forwarding scope Forward only the specific private + privatelink names (conditional forwarding), not a blanket *, to keep public resolution predictable.
Policy as guardrail Use Azure Policy to force private DNS integration on Private Endpoints. Deploy-if-not-exists is stronger than a runbook.
Audit and diagnostics Record-set changes flow to the Activity log; alert on changes in sensitive zones to catch hijacks or fat-fingers.

For the Private Link side of this story, Azure Private Link and Private DNS: Keeping PaaS Off the Public Internet goes deeper on locking the data plane down.

Cost & sizing

Private DNS is one of the cheapest services in Azure and for most workloads rounds to free — but know what meters.

Cost driver How it’s charged Typical magnitude Notes
Private DNS zone (hosting) Per zone per month A few US cents per zone First 25 zones at the low per-zone rate; negligible
DNS queries Per million queries Cents per million VNet-internal queries are cheap; rarely noticed
Record sets Included in zone hosting No separate per-record charge
VNet links No separate charge Free Link to as many VNets as needed
Auto-registration No separate charge Free Part of the zone/link
Azure DNS Private Resolver (optional) Endpoint-hour + per million queries The real cost for on-prem resolution Inbound + outbound endpoints each meter

Sizing is simple: zones, links, records, and auto-registration are not a cost concern — provision them freely and prefer many resolution-only links over manual records. The one line item that grows is the Azure DNS Private Resolver, billed per endpoint-hour, added only when on-prem ↔ Azure private resolution is required — and even then far cheaper than a pair of HA DNS-forwarder VMs. There is no free-tier limit to worry about because per-zone and per-query costs are trivial; for this article’s lab the only real charges are the VMs and storage you delete at the end.

Interview & exam questions

1. What is an Azure Private DNS zone and how does it differ from Azure DNS (public)? A private zone hosts DNS records that are never published to the public internet and resolve only for the virtual networks you link to the zone. Azure DNS public hosts internet-facing zones resolvable by anyone. Same product family, opposite audience: private = linked VNets only; public = the whole internet.

2. How does a resource in a VNet actually resolve a name from a private zone — do you point it at the zone? No. Resources query the Azure-provided resolver at 168.63.129.16 (on default DNS), which checks the linked private zones first (longest suffix match), answers privately if a record exists, else recurses to public DNS. You never configure clients to use the zone directly; you create the link.

3. What is auto-registration and what is its key constraint? A per-link flag that makes VMs in the registration VNet automatically create, update, and remove their own A records as they’re created, change IP, or are deleted. The constraint: a VNet can be the registration VNet for only one private zone; all its other links must be resolution-only.

4. Why might a Private Endpoint still resolve to a public IP even though it’s healthy? Because the privatelink.* zone holding its A record is not linked to the VNet doing the query (or the suffix is wrong, or the VNet uses custom DNS that bypasses 168.63.129.16). Confirm with nslookup from the actual consuming VM and check the zone’s links. Fix by linking the zone (resolution-only) to that VNet.

5. What is split-horizon DNS on Azure and how do you implement it? It’s making the same FQDN return a private answer inside linked VNets and a public answer everywhere else. You implement it by creating a private zone with the same name as your public zone (e.g. both kloudvin.com) and linking the private one only to the internal VNets. The two zones are independent; Azure never publishes the private one.

6. Why doesn’t VNet peering give peered VMs access to a private zone’s records? Because peering moves traffic, not DNS. Resolution is decided per-VNet by that VNet’s links, and a link is not transitive across peering — a peered VNet needs its own link to the zone.

7. A network team set custom DNS servers on the VNet and all private records stopped resolving. Why, and what are the fixes? Queries go to those servers, not 168.63.129.16, so the private zones are never consulted. Fixes: revert to default DNS; make the custom servers conditionally forward the private + privatelink names to 168.63.129.16; or place an Azure DNS Private Resolver in the path.

8. Which record types can a private zone hold, and what’s the apex restriction? A, AAAA, CNAME, MX, PTR, SRV, TXT, plus an auto-created SOA. The apex restriction is standard: no CNAME at @ because the apex must hold the SOA — use an A record at the apex instead.

9. What is a DNS zone group on a Private Endpoint and why prefer it? An object that ties the endpoint to a private zone so the correct A record is created and kept in sync automatically (including IP changes on recreation). Prefer it over hand-adding the record, which goes stale and breaks resolution silently.

10. How do you let on-premises clients resolve an Azure private zone name? On-prem can’t reach 168.63.129.16 directly. Deploy an Azure DNS Private Resolver with an inbound endpoint (a private IP your on-prem DNS forwards to), plus an outbound endpoint + forwarding ruleset if Azure needs on-prem zones. This replaces the legacy DNS-forwarder-VM pattern.

11. In a hub-and-spoke topology, where should the privatelink.* zones live and how are they linked? Centralise them in the hub (one zone per service) with resolution-only links to every spoke that consumes the endpoints. Use Azure Policy to auto-deploy the DNS zone group on each Private Endpoint so the wiring is never forgotten.

12. What drives the cost of Private DNS, and where can a real bill appear? Zones, links, records, and auto-registration are effectively free (tiny per-zone/per-query charges). The only meaningful cost is the optional Azure DNS Private Resolver, billed per endpoint-hour plus per-query, added only for on-prem ↔ Azure resolution.

These map to AZ-104 (configure name resolution, Private DNS zones, VNet integration) and AZ-700 (design and implement private access to Azure services, Private Endpoints, Private DNS) — split-horizon and privatelink show up most on AZ-700. A compact cert-mapping for revision:

Question theme Primary cert Objective area
Private zones, links, auto-registration AZ-104 Configure name resolution
Private Endpoint + privatelink zones AZ-700 Private access to Azure services
Split-horizon, hub-spoke DNS design AZ-700 Design private connectivity
Custom DNS + forwarding to 168.63.129.16 AZ-700 / AZ-104 Name resolution for VNets
DNS Private Resolver (on-prem ↔ Azure) AZ-700 Hybrid name resolution
Azure Policy for DNS guardrails AZ-500 / AZ-104 Governance of network resources

Quick check

  1. You leave a VNet on default DNS. A resource queries a name. Which IP does the query go to, and what does that resolver check before recursing to public DNS?
  2. A Private Endpoint is healthy but the app resolves the storage account to a public IP. Name the single most likely cause and the command you’d run to confirm it.
  3. True or false: enabling auto-registration on a link in the hub VNet makes VMs in a peered spoke auto-register into that zone.
  4. You want app.kloudvin.com to return 10.20.1.10 inside the VNet but the public Front Door IP on the internet. What do you create, and what’s the one rule about its name?
  5. Your network team set the VNet to use two custom DNS server IPs, and now no private records resolve. Why, and what’s the fix that keeps the custom servers?

Answers

  1. The query goes to the Azure-provided resolver at 168.63.129.16. It checks the private DNS zones linked to that VNet (longest suffix match) first, answers privately if a record exists, and only then recurses to public DNS.
  2. The privatelink.blob.core.windows.net zone is not linked to the consumer’s VNet (so resolution falls through to public). Confirm by running nslookup <account>.blob.core.windows.net from the actual consuming VM and listing the zone’s links with az network private-dns link vnet list --zone-name privatelink.blob.core.windows.net. Fix by linking the zone (resolution-only) to that VNet.
  3. False. Peering moves traffic, not DNS, and auto-registration is per-VNet — only VMs in the registration VNet auto-register. A peered spoke needs its own registration zone (and a VNet can have only one registration link).
  4. Create a private DNS zone named kloudvin.com (split-horizon shadow of the public zone), link it to the internal VNets, and add an A record app → 10.20.1.10. The rule: the private zone must have the same name as the public zone, and it stays unlinked from anywhere that should see the public answer.
  5. The custom DNS servers receive the queries instead of 168.63.129.16, so the private zones are never consulted. Keep the custom servers by configuring conditional forwarding of the private zone names (and every privatelink.* name) to 168.63.129.16, or front them with an Azure DNS Private Resolver.

Glossary

Next steps

You can now create a private zone, link it to VNets, auto-register VMs, run split-horizon, and make Private Endpoints resolve. Build outward:

AzurePrivate DNSDNSPrivate EndpointVirtual NetworkNetworkingBicepSplit-Horizon
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading