VNet Peering & Private Endpoints
Why this matters
Real Azure networks are never one VNet. Peering is how you connect them over Azure's private backbone (no public internet, no VPN), and it's the foundation of the hub-and-spoke topology you'll build in module 07 and the capstone. Private Endpoints are how you pull PaaS services (Storage, SQL, Key Vault) off the public internet and onto a private IP inside your VNet — and they depend directly on the private DNS you learned in module 03. Get peering's non-transitivity and private-endpoint DNS wrong and connections fail in ways that look like everything but the real cause.
Concepts
VNet peering
Peering connects two VNets so resources in each can reach the other by private
IP, as if on one network, using Azure's backbone (low latency, no gateway
required for same-region peering). It's the "routed connection between two
networks" from track 1. Peering is created as two links — one from each
VNet's side — and both must exist for traffic to flow. The hard prerequisite:
the two VNets' address spaces must not overlap (this is why module 01
insisted on non-overlapping /16s).
Peering is NOT transitive
This is the concept that catches everyone. If spoke-A peers with a hub, and
spoke-B peers with the same hub, spoke-A cannot reach spoke-B through the
hub by default. Peering only connects the two directly-peered VNets. To let
spokes talk to each other you must either peer them directly, or route their
traffic through a device in the hub (Azure Firewall or an NVA) using route
tables and the allowForwardedTraffic / gateway-transit settings. Hub-and-spoke
in module 07 relies on exactly this: spokes reach each other via the hub
firewall, not via peering transitivity.
Peering settings that matter
Each side of a peering has toggles: --allow-vnet-access (permit traffic at
all), --allow-forwarded-traffic (accept traffic that originated elsewhere and
was forwarded — needed for hub routing), and gateway transit
(--allow-gateway-transit / --use-remote-gateways, for sharing a VPN/
ExpressRoute gateway in the hub). For simple spoke-to-hub connectivity you need
vnet-access on both sides; for spoke-to-spoke-via-hub you also need forwarded
traffic allowed.
Private Endpoints
A Private Endpoint gives a PaaS service (Storage account, SQL DB, Key Vault, etc.) a private IP inside your subnet, so your resources reach it over the VNet/backbone instead of its public endpoint. It creates a NIC in your subnet mapped to that specific service instance. This removes the service from public exposure — you can then disable its public endpoint entirely. It's the private-connectivity counterpart to service endpoints (which keep the service's public IP but route to it privately); private endpoints are the stronger, now-preferred pattern.
Private Endpoints depend on private DNS
The catch: your app still connects using the service's public hostname
(e.g. myacct.blob.core.windows.net). For that name to resolve to the private
IP instead of the public one, you need a private DNS zone named exactly for the
service's privatelink domain (e.g. privatelink.blob.core.windows.net),
linked to your VNet, with an A record for the endpoint. Azure can auto-create
this via a private DNS zone group on the endpoint. Skip the DNS piece and
the name keeps resolving to the public IP — the private endpoint exists but
nothing uses it. This is module 03's "zone must be linked" lesson with higher
stakes.
Command reference
| Command | What it does | Example |
|---|---|---|
az network vnet peering create |
Creates one side of a peering | az network vnet peering create -g pl-lab-rg -n hub-to-spokeA --vnet-name hub-vnet --remote-vnet spokeA-vnet --allow-vnet-access |
az network vnet peering list |
Lists peerings on a VNet | az network vnet peering list -g pl-lab-rg --vnet-name hub-vnet -o table |
az network vnet peering show |
Shows a peering (incl. state) | az network vnet peering show -g pl-lab-rg -n hub-to-spokeA --vnet-name hub-vnet |
az network private-endpoint create |
Creates a private endpoint to a PaaS resource | az network private-endpoint create -g pl-lab-rg -n stg-pe --vnet-name spokeA-vnet --subnet app-subnet --private-connection-resource-id <storage-id> --group-id blob --connection-name stg-conn |
az network private-endpoint dns-zone-group create |
Wires the endpoint to a private DNS zone | az network private-endpoint dns-zone-group create -g pl-lab-rg --endpoint-name stg-pe -n default --private-dns-zone privatelink.blob.core.windows.net --zone-name blob |
az network private-dns zone create |
Creates the privatelink zone | az network private-dns zone create -g pl-lab-rg -n privatelink.blob.core.windows.net |
Flag breakdown — az network vnet peering create -g pl-lab-rg -n hub-to-spokeA --vnet-name hub-vnet --remote-vnet spokeA-vnet --allow-vnet-access:
-n(--name): a name for this side of the peering.--vnet-name: the local VNet this link is created on.--remote-vnet: the other VNet (name in same group, or full resource ID across groups/subscriptions).--allow-vnet-access: permit traffic between the two VNets. (Add--allow-forwarded-trafficwhen the hub must forward spoke-to-spoke traffic.)
Flag breakdown — az network private-endpoint create ... --private-connection-resource-id <storage-id> --group-id blob --connection-name stg-conn:
--vnet-name/--subnet: where the endpoint's NIC/private IP lands.--private-connection-resource-id: the full resource ID of the PaaS resource you're connecting to.--group-id(sub-resource): which part of the service (e.g.blob,file,sqlServer) — determines the privatelink zone name you need.--connection-name: a name for the connection.
Hands-on exercises
Set up a group and three VNets with non-overlapping address spaces:
az group create --name pl-lab-rg --location eastus
az network vnet create -g pl-lab-rg -n hub-vnet --address-prefixes 10.0.0.0/16 \
--subnet-name hub-subnet --subnet-prefixes 10.0.1.0/24
az network vnet create -g pl-lab-rg -n spokeA-vnet --address-prefixes 10.1.0.0/16 \
--subnet-name app-subnet --subnet-prefixes 10.1.1.0/24
az network vnet create -g pl-lab-rg -n spokeB-vnet --address-prefixes 10.2.0.0/16 \
--subnet-name app-subnet --subnet-prefixes 10.2.1.0/24
Peer hub ↔ spokeA (both sides).
az network vnet peering create -g pl-lab-rg -n hub-to-spokeA \ --vnet-name hub-vnet --remote-vnet spokeA-vnet --allow-vnet-access az network vnet peering create -g pl-lab-rg -n spokeA-to-hub \ --vnet-name spokeA-vnet --remote-vnet hub-vnet --allow-vnet-accessVerify:
az network vnet peering show -g pl-lab-rg -n hub-to-spokeA --vnet-name hub-vnet --query peeringStatereturnsConnected. Only one side created =Initiated, notConnected.Peer hub ↔ spokeB (both sides). Repeat the pattern for spokeB.
az network vnet peering create -g pl-lab-rg -n hub-to-spokeB \ --vnet-name hub-vnet --remote-vnet spokeB-vnet --allow-vnet-access az network vnet peering create -g pl-lab-rg -n spokeB-to-hub \ --vnet-name spokeB-vnet --remote-vnet hub-vnet --allow-vnet-accessDiagnose: peering stuck at
Initiated. If you created only one side of a peering (or the second side names the wrong remote VNet), the state staysInitiatedand traffic doesn't flow. Fix: create the matching link on the other VNet with the correct--remote-vnet. Both links must exist and reference each other forConnected.Verify: both
hub-to-spokeAandspokeA-to-hubshowConnected.Prove non-transitivity (reason it through). spokeA is peered to hub, and spokeB is peered to hub, but spokeA and spokeB are not peered to each other. A VM in spokeA therefore cannot reach a VM in spokeB — peering is not transitive. Confirm there is no direct spokeA↔spokeB peering:
az network vnet peering list -g pl-lab-rg --vnet-name spokeA-vnet -o tableYou'll see only
spokeA-to-hub. (In module 07 you'll route spoke-to-spoke through the hub firewall instead of peering them directly.)Diagnose and fix: overlapping address space blocks peering. Try to create a fourth VNet that overlaps the hub and peer it:
az network vnet create -g pl-lab-rg -n bad-vnet \ --address-prefixes 10.0.0.0/16 --subnet-name s1 \ --subnet-prefixes 10.0.5.0/24 az network vnet peering create -g pl-lab-rg -n hub-to-bad \ --vnet-name hub-vnet --remote-vnet bad-vnet --allow-vnet-accessThe peering fails because
bad-vnet(10.0.0.0/16) overlapshub-vnet. Fix: overlapping VNets can't be peered — the only remedy is to re-address one of them. Deletebad-vnet.Create a Storage account and a private endpoint to it. (Storage bills minimally; delete with the group.)
STG=stg$RANDOM$RANDOM az storage account create -g pl-lab-rg -n $STG --sku Standard_LRS STGID=$(az storage account show -g pl-lab-rg -n $STG --query id -o tsv) az network private-endpoint create -g pl-lab-rg -n stg-pe \ --vnet-name spokeA-vnet --subnet app-subnet \ --private-connection-resource-id "$STGID" --group-id blob \ --connection-name stg-connVerify:
az network private-endpoint show -g pl-lab-rg -n stg-pe --query 'customDnsConfigs'shows a private IP fromapp-subnet(10.1.1.x).Diagnose and fix: private endpoint exists but the name still resolves public. Right now there's no private DNS, so
<acct>.blob.core.windows.netstill resolves to the public IP — the private endpoint is unused. Fix: create the privatelink zone, link it to the VNet, and attach a DNS zone group so Azure creates the A record:az network private-dns zone create -g pl-lab-rg \ -n privatelink.blob.core.windows.net az network private-dns link vnet create -g pl-lab-rg \ -z privatelink.blob.core.windows.net -n link-spokeA \ --virtual-network spokeA-vnet --registration-enabled false az network private-endpoint dns-zone-group create -g pl-lab-rg \ --endpoint-name stg-pe -n default \ --private-dns-zone privatelink.blob.core.windows.net --zone-name blobVerify: the private zone now has an A record for the storage account pointing at the endpoint's private IP. From a VM in spokeA,
nslookup <acct>.blob.core.windows.netwould return the10.1.1.xprivate IP instead of a public one. Lesson: a private endpoint without its privatelink DNS zone (linked to the VNet) is invisible — the name keeps resolving to the public endpoint.Cleanup. Peerings are free, but the storage account and private endpoint carry small charges. Delete the group:
az group delete --name pl-lab-rg --yes --no-wait
Independent challenge
Combine this module with module 03 (private DNS). Build two VNets with
non-overlapping address spaces, peer them both directions, create a private
endpoint to a new Key Vault (or Storage account) in one VNet, and wire up the
correct privatelink.* private DNS zone linked so that a resource in the
peered VNet — not just the endpoint's own VNet — resolves the service to its
private IP. Confirm you understand which VNets must be linked to the
privatelink zone for cross-VNet private resolution to work. Delete the resource
group afterward — the endpoint and PaaS resource bill while they exist.
Stuck? One hint
For a peered VNet to resolve the private endpoint privately, the
privatelink.* zone must be linked to that VNet too (or the two VNets must
share a resolver that sees the zone). Peering routes the traffic; the private
DNS link is what makes the name resolve to the private IP. Both are required
— test resolution from the peered VNet specifically, not just from the VNet
holding the endpoint.
Common mistakes & troubleshooting
- Only creating one side of a peering. State stays
Initiated; you need both links, each pointing at the other, forConnected. - Overlapping address spaces. Peering simply won't establish; re-address one VNet — there's no override.
- Expecting transitivity. spoke-A → hub → spoke-B does not work by default. Peer directly or route through a hub device (module 07).
- Private endpoint without private DNS. The endpoint exists but the service
name still resolves to the public IP, so nothing uses the private path. You
need the
privatelink.*zone, linked to the VNet, with the A record. - Forgetting
allow-forwarded-trafficfor hub routing. Spoke-to-spoke via a hub NVA/firewall needs forwarded traffic allowed on the peerings plus route tables (module 07). - Cost note: peerings themselves are free (you pay a small per-GB data transfer for traffic across them), but private endpoints bill a small hourly rate plus per-GB, and the PaaS resources bill on their own. Delete the group when done.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- spoke-A and spoke-B are each peered to a hub. Can a VM in spoke-A reach a VM in spoke-B by default? Explain, and give two ways to make it work.
- A peering shows
Initiated, notConnected. What's the cause and fix? - Why can't you peer two VNets that both use
10.0.0.0/16? - What does a Private Endpoint give a PaaS service, and how does that differ from a service endpoint?
- You created a private endpoint to a storage account but connections still go to the public IP. What's missing, precisely?
- What exactly must the private DNS zone for a blob private endpoint be named, and what has to be linked to it?
- Which peering setting must be enabled for a hub to forward spoke-to-spoke traffic through a firewall?
Show answers
- No — peering is not transitive. Make it work by (a) peering spoke-A and
spoke-B directly, or (b) routing spoke-to-spoke through a hub device
(Firewall/NVA) using route tables plus
allow-forwarded-traffic. - Only one side of the peering exists (or the second side references the wrong
remote VNet). Create the matching link on the other VNet pointing back; both
are needed for
Connected. - Overlapping address spaces can't be routed between — Azure can't decide
which VNet owns
10.0.0.0/16. Re-address one. - A Private Endpoint gives the service a private IP (a NIC) inside your subnet, removing it from public exposure. A service endpoint keeps the service's public IP but routes to it over the backbone from a specific subnet — weaker isolation; private endpoints are preferred.
- The privatelink private DNS zone, linked to the VNet, with the A record for the endpoint (a private DNS zone group). Without it the public hostname still resolves to the public IP.
privatelink.blob.core.windows.net, and the VNet(s) that need to resolve it must be linked to that zone (plus the endpoint's A record present).--allow-forwarded-trafficon the relevant peering(s), alongside route tables directing spoke traffic to the hub device.
Next
06 — Application Gateway & WAF: move up to L7 load balancing with path-based routing, TLS termination, and a Web Application Firewall.