Your Key Vault and storage account (Ch 22) have public addresses: anyone on the internet can reach them and is stopped only by authentication. Security wants them reachable only from your VNet. A private endpoint gives the service an address inside your subnet so you can switch the public one off - and then DNS decides whether anything still works.
What you need to know already: 23.1 (subnets, NICs), 8.16-8.22 (DNS: A and CNAME records, resolvers, split horizon), 22.23 (Key Vault), 22.29 (storage accounts and blob), 9.15 (TLS and certificate names).
What a private endpoint is
A private endpoint (PE) is a NIC in your subnet with a private IP that maps to one specific PaaS resource (platform as a service: a service Azure runs for you, like one vault or one storage account's blob service). Traffic to that IP stays on the Microsoft backbone (Microsoft's own network between its data centres) and never touches the resource's public endpoint - which you can then switch off. The feature behind it is called Private Link.
az network private-endpoint show ... --query below prints the endpoint's IP, the name it serves, and whether the connection was approved by the resource.
# pe-blob and its DNS zone come from the next mission
az network private-endpoint show -g rg-oncall-lab -n pe-blob --query "{ip:customDnsConfigs[0].ipAddresses[0], fqdn:customDnsConfigs[0].fqdn, state:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}"
{
"fqdn": "stsysoplab.blob.core.windows.net",
"ip": "10.20.3.4",
"state": "Approved"
}
The part everyone gets wrong: DNS
The application still connects to stsysoplab.blob.core.windows.net (TLS certificates and SDKs require the real name). So that name has to resolve to 10.20.3.4 from inside your VNet, and to the public IP from everywhere else. This is split horizon DNS (8.22): the same name, different answers inside and outside. Azure does it with a CNAME chain:
stsysoplab.blob.core.windows.net
CNAME stsysoplab.privatelink.blob.core.windows.net (added when a PE exists)
CNAME blob.ams07prdstr02a.store.core.windows.net (public answer)
A private DNS zone is a DNS zone (a set of records for one domain) that only the VNets you link to it can see. If the VNet can see a private DNS zone called privatelink.blob.core.windows.net with an A record stsysoplab -> 10.20.3.4, the chain stops there with the private IP. If it cannot, resolution continues to the public IP - and the connection either goes over the internet or, once public access is disabled, fails with a 403 that looks like an auth problem.
The zone names are fixed per service:
privatelink.blob.core.windows.net blob
privatelink.file.core.windows.net file
privatelink.vaultcore.azure.net Key Vault
privatelink.azurecr.io Container Registry (ACR, Azure's image registry, Ch 10)
privatelink.westeurope.azmk8s.io AKS private cluster API (23.20)
privatelink.database.windows.net Azure SQL
Three pieces, all required
- the zone -
az network private-dns zone create -n <zone name>; - a VNet link -
az network private-dns link vnet create: lets one VNet see the zone (--registration-enabled false: do not auto-add VM names); - the A record - best made by a zone group (
az network private-endpoint dns-zone-group create), which ties the record to the endpoint.
# 1. the zone (usually once, in a central "hub" subscription)
az network private-dns zone create -g rg-oncall-lab -n privatelink.blob.core.windows.net
# 2. link it to every VNet whose clients must resolve privately
az network private-dns link vnet create -g rg-oncall-lab --zone-name privatelink.blob.core.windows.net \
-n link-vnet-sysop --virtual-network vnet-sysop --registration-enabled false
# 3. the A record - let a zone group maintain it for you
az network private-endpoint dns-zone-group create -g rg-oncall-lab --endpoint-name pe-blob \
-n default --private-dns-zone privatelink.blob.core.windows.net --zone-name blob
# after the private-endpoint mission
az network private-dns record-set a list -g rg-oncall-lab -z privatelink.blob.core.windows.net -o table
Name ResourceGroup Ttl Type AutoRegistered Metadata
---------- --------------- ----- ------ ---------------- ----------
stsysoplab rg-oncall-lab 10 A False
A zone group creates and deletes the A record together with the endpoint; a hand-made record goes stale the day someone recreates the endpoint and it gets a different IP.
Failure modes, in order of frequency:
- zone exists, no VNet link -> public IP from inside the VNet
- linked to the app VNet but the app resolves through a custom DNS server (on-prem, a DNS VM) that does not forward (pass on)
privatelink.*questions to Azure DNS (168.63.129.16) -> public IP - record points at an old IP (hand-made records)
- the zone is created per team in every subscription -> split-brain (two copies that disagree); one central zone per service, linked everywhere, is the pattern
Then turn the public door off
az storage account update -n stsysoplab --public-network-access Disabled
az keyvault update -n kv-shop-prod --public-network-access Disabled
Only private endpoints work afterwards. Your laptop, the portal's data browser and any automation running on machines outside the VNet lose access too - plan how they get in (automation machines inside the VNet, a jump host, Bastion).
Service endpoints: the older, weaker option
A service endpoint (Microsoft.Storage enabled on a subnet) lets that subnet reach the service's public endpoint over the backbone, and the service's firewall can allow "subnet X". No private IP, no DNS change, free - but the service stays public, it works per service type rather than per resource (data exfiltration - someone copying your data out - to any storage account is possible), and it does not extend to on-prem. New designs use private endpoints.
How to prove the resolution from inside
From a pod or VM in the VNet:
# from a VM inside the linked VNet
nslookup stsysoplab.blob.core.windows.net
Non-authoritative answer:
stsysoplab.blob.core.windows.net canonical name = stsysoplab.privatelink.blob.core.windows.net.
Name: stsysoplab.privatelink.blob.core.windows.net
Address: 10.20.2.4
From outside (your laptop, or oncall-lab here), the same name answers with the public IP after the privatelink CNAME - the CNAME is public, the A record behind it only exists in your private zone.
If the answer inside the VNet is public:
# the checklist, once the private endpoint exists
az network private-dns link vnet list -g rg-oncall-lab -z privatelink.blob.core.windows.net -o table
az network private-dns record-set a list -g rg-oncall-lab -z privatelink.blob.core.windows.net -o table
az network private-endpoint show -g rg-oncall-lab -n pe-blob-app --query "customDnsConfigs[0].ipAddresses[0]" -o tsv
Link missing, record missing, or record IP different from the endpoint IP - those three commands cover almost every case. The drill after the Key Vault mission trains exactly this triage.
One endpoint per sub-resource
A storage account has separate endpoints per service: blob, file, queue, table, dfs, web. A private endpoint with --group-id blob does nothing for Azure Files - an AKS cluster mounting Azure Files privately needs a second endpoint with --group-id file and the privatelink.file.core.windows.net zone. (--group-id names the sub-resource the endpoint is for.)
What you can now do
- Create a private endpoint and the three DNS pieces it needs.
- Explain why the app still uses the public name, and how the CNAME chain turns it into a private IP inside the VNet.
- Triage "resolves to the public IP" with three list commands.