Home / Learning

LearningLearningBeginner

Learning: Azure networking for beginners - VNets, subnets, peering, NSGs, private endpoints and private DNS, built and tested with the Azure CLI

A hands-on learning session. Build two VNets with VMs that have no public IPs, prove they can't talk, peer them, control traffic with NSG rules and read the effective rules, then reach a storage account over a private endpoint, see private DNS working (and failing), and switch off public access. Exercises and a knowledge check included.

Deepak Bhardwaj11 min read

Applies toAzure Virtual Network, Network Security Groups, VNet peering, Private Endpoint, Azure Private DNS, Network Watcher; Azure CLI 2.60 or later; Ubuntu 22.04 VMs

About this session

Session Details
Duration 2–3 hours, including exercises
You will build Two peered VNets, two small Linux VMs without public IPs, an NSG with your own rules, and a storage account reachable only through a private endpoint with private DNS
You will learn Address spaces and subnets, why VNets are isolated, peering, NSG rules and default rules, effective rules and IP flow verify, testing from inside a VM with Run Command, private endpoints, private DNS zones and VNet links, disabling public network access
Prerequisites An Azure subscription where you can create resources; Azure CLI installed and az login done; basic networking (IP addresses, CIDR, ports)

Note

The lab uses two B1s VMs and one private endpoint for a couple of hours: a small cost. Delete the resource group at the end (Module 10). Check current pricing for your region if cost matters.

Module 1: the ideas you need first

Building block What it is
VNet A private network in one region with one or more address spaces, e.g. 10.10.0.0/16
Subnet A slice of the VNet, e.g. 10.10.1.0/24. Azure reserves 5 addresses in each
NSG A stateful firewall rule list attached to a subnet or NIC
Peering A private, low-latency link between two VNets. Not transitive
Private endpoint A NIC with a private IP in your subnet that connects to one PaaS resource (e.g. a storage account's blob service)
Private DNS zone A DNS zone visible only to linked VNets, e.g. privatelink.blob.core.windows.net

Two VNets are isolated by default: no traffic flows between them until you peer them (or connect them another way).

What you will build:

 vnet-app 10.10.0.0/16                         vnet-shared 10.20.0.0/16
 ┌──────────────────────────────┐   peering    ┌───────────────────────┐
 │ snet-app 10.10.1.0/24  [NSG] │◄────────────►│ snet-shared 10.20.1.0/24│
 │   vm-app (no public IP)      │              │   vm-shared (no public IP)│
 │ snet-pe 10.10.2.0/24         │              └───────────────────────┘
 │   pe-blob ──► storage account (public access off)
 └──────────────────────────────┘
       private DNS zone: privatelink.blob.core.windows.net (linked to vnet-app)

Module 2: resource group and VNets

RG=rg-learn-net
LOC=uksouth            # pick a region near you
az group create -n $RG -l $LOC

az network vnet create -g $RG -n vnet-app --address-prefixes 10.10.0.0/16 \
  --subnet-name snet-app --subnet-prefixes 10.10.1.0/24
az network vnet subnet create -g $RG --vnet-name vnet-app -n snet-pe --address-prefixes 10.10.2.0/24

az network vnet create -g $RG -n vnet-shared --address-prefixes 10.20.0.0/16 \
  --subnet-name snet-shared --subnet-prefixes 10.20.1.0/24

az network vnet list -g $RG --query "[].{name:name, space:addressSpace.addressPrefixes[0]}" -o table

Tip

Plan address spaces so they never overlap with each other or with on-premises ranges. Overlapping VNets can't be peered, and fixing it later means rebuilding.

Module 3: two VMs without public IPs

for v in app shared; do
  az vm create -g $RG -n vm-$v --image Ubuntu2204 --size Standard_B1s \
    --vnet-name vnet-$v --subnet snet-$v \
    --public-ip-address "" --nsg "" \
    --admin-username azureuser --generate-ssh-keys --no-wait
done
az vm wait -g $RG -n vm-app --created && az vm wait -g $RG -n vm-shared --created

az vm list-ip-addresses -g $RG \
  --query "[].{vm:virtualMachine.name, ip:virtualMachine.network.privateIpAddresses[0]}" -o table

Note the private IPs (usually 10.10.1.4 and 10.20.1.4). With no public IP you can't SSH in from your laptop, so you'll test with Run Command, which runs a script inside the VM through the Azure agent:

run() { az vm run-command invoke -g $RG -n $1 --command-id RunShellScript --scripts "$2" \
          --query "value[0].message" -o tsv; }
run vm-app "hostname; ip -4 addr show eth0 | grep inet"

Module 4: prove the VNets are isolated

Start a small web server on vm-app, then try to reach it from vm-shared:

run vm-app "nohup python3 -m http.server 8080 >/dev/null 2>&1 & sleep 1; ss -ltn | grep 8080"
run vm-shared "nc -zvw3 10.10.1.4 8080; nc -zvw3 10.10.1.4 22"

Both fail with a timeout: there's no route between the two address spaces.

Module 5: peer the VNets

Peering is created from both sides:

az network vnet peering create -g $RG -n app-to-shared --vnet-name vnet-app \
  --remote-vnet vnet-shared --allow-vnet-access
az network vnet peering create -g $RG -n shared-to-app --vnet-name vnet-shared \
  --remote-vnet vnet-app --allow-vnet-access

az network vnet peering list -g $RG --vnet-name vnet-app --query "[].{name:name,state:peeringState}" -o table

peeringState must be Connected on both sides. Test again:

run vm-shared "nc -zvw3 10.10.1.4 8080; nc -zvw3 10.10.1.4 22; curl -s -o /dev/null -w '%{http_code}\n' http://10.10.1.4:8080/"

Now it works. Look at the routes Azure created on vm-app's NIC:

NIC=$(az vm show -g $RG -n vm-app --query "networkProfile.networkInterfaces[0].id" -o tsv)
az network nic show-effective-route-table --ids $NIC -o table

You'll see a route for 10.20.0.0/16 with next hop type VNetPeering.

Module 6: control traffic with an NSG

No NSG is attached yet, so everything inside the peered networks is allowed. Create one for snet-app that allows only SSH from vnet-shared and denies other traffic from the virtual network:

az network nsg create -g $RG -n nsg-app

az network nsg rule create -g $RG --nsg-name nsg-app -n allow-ssh-from-shared \
  --priority 100 --direction Inbound --access Allow --protocol Tcp \
  --source-address-prefixes 10.20.0.0/16 --destination-port-ranges 22

az network nsg rule create -g $RG --nsg-name nsg-app -n deny-vnet-inbound \
  --priority 200 --direction Inbound --access Deny --protocol '*' \
  --source-address-prefixes VirtualNetwork --destination-address-prefixes '*' --destination-port-ranges '*'

az network vnet subnet update -g $RG --vnet-name vnet-app -n snet-app --network-security-group nsg-app

Rules are checked by priority, lowest number first, and the first match wins. Every NSG also has default rules at priority 65000+ (AllowVnetInBound, AllowAzureLoadBalancerInBound, DenyAllInBound), which is why traffic between peered VNets was allowed before.

Test:

run vm-shared "nc -zvw3 10.10.1.4 22; nc -zvw3 10.10.1.4 8080"

Port 22 succeeds; 8080 now times out. Compare with a port where nothing listens but the NSG allows (after the exercises): you'd get connection refused immediately. Timeout usually means a firewall dropped it; refused means it got there and nothing was listening.

See which rule decided, without guessing:

az network nic list-effective-nsg --ids $NIC \
  --query "value[0].effectiveSecurityRules[?direction=='Inbound'].{name:name,prio:priority,access:access,src:sourceAddressPrefix,port:destinationPortRange}" -o table

az network watcher test-ip-flow -g $RG --vm vm-app --direction Inbound --protocol TCP \
  --local 10.10.1.4:8080 --remote 10.20.1.4:50000

test-ip-flow answers Allow or Deny and names the rule. It's the fastest way to settle "is the NSG blocking it?" (Network Watcher is enabled automatically in most subscriptions; if the command complains, run az network watcher configure -g NetworkWatcherRG -l $LOC --enabled true.)

Module 7: a storage account behind a private endpoint

SA=stlearn$RANDOM$RANDOM
az storage account create -g $RG -n $SA -l $LOC --sku Standard_LRS --min-tls-version TLS1_2
SA_ID=$(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-app --subnet snet-pe \
  --private-connection-resource-id $SA_ID --group-id blob --connection-name pe-blob-conn

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-vnet-app -v vnet-app -e false
az network private-endpoint dns-zone-group create -g $RG --endpoint-name pe-blob -n default \
  --private-dns-zone privatelink.blob.core.windows.net --zone-name blob

The zone group creates an A record for your account in the private zone and keeps it in sync with the endpoint's IP.

Module 8: watch DNS do its job (and fail)

run vm-app    "nslookup $SA.blob.core.windows.net"
run vm-shared "nslookup $SA.blob.core.windows.net"
  • From vm-app: the name resolves through a privatelink CNAME to 10.10.2.x, the private endpoint.
  • From vm-shared: it resolves to a public IP, because the private zone isn't linked to vnet-shared. Peering doesn't share DNS zones.

This is the single most common private endpoint problem. Fix it by linking the zone to the other VNet:

az network private-dns link vnet create -g $RG -z privatelink.blob.core.windows.net \
  -n link-vnet-shared -v vnet-shared -e false
run vm-shared "nslookup $SA.blob.core.windows.net"

Now both resolve privately. (In real hub-and-spoke designs, the zones live in the hub and are linked to every spoke, or resolved through a DNS forwarder or Private DNS Resolver; see private endpoint DNS.)

Module 9: turn off public access

az storage account update -g $RG -n $SA --public-network-access Disabled

# From your laptop (public internet) - should now fail
az storage container list --account-name $SA --auth-mode key -o table

The request from your laptop is refused because public network access is disabled. From inside vm-app, the private endpoint still works:

run vm-app "curl -s -o /dev/null -w '%{http_code}\n' https://$SA.blob.core.windows.net/?comp=list"

You'll get an HTTP status from the storage service (for example 400 or 403 because the request has no credentials). The point is that the connection reached it privately; with no private path you'd get a timeout or connection error instead.

Module 10: clean up

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

Everything (VMs, disks, NICs, VNets, NSG, private endpoint, DNS zone, storage) is in the one resource group, so this removes it all.

Exercises

  1. Add an NSG rule that allows port 8080 from 10.20.1.4/32 only, at priority 150. Prove it with nc and test-ip-flow. Why must its priority be lower than 200?
  2. Create a third VNet vnet-spoke2 peered only to vnet-shared. Can vnet-spoke2 reach vnet-app? Explain using the word "transitive".
  3. Remove the DNS zone group from pe-blob and recreate the A record by hand. What breaks if the endpoint's IP ever changes?
  4. Add an outbound NSG rule on snet-app that denies Internet and allows Storage, using service tags. What still works from vm-app?
  5. Attach an NSG to vm-app's NIC as well as the subnet. In which order are subnet and NIC NSGs evaluated for inbound traffic?

Knowledge check

  1. Why couldn't the two VMs talk before peering?
  2. In what order are NSG rules evaluated, and what happens after a match?
  3. Why was traffic between peered VNets allowed before you added your own NSG rules?
  4. What's the practical difference between a timeout and "connection refused" when testing a port?
  5. Why did the storage name resolve to a public IP from vm-shared at first?
  6. What does disabling public network access on the storage account change?
Answers
  1. VNets are isolated by default; there was no route between their address spaces until peering added one.
  2. By priority, lowest number first. The first matching rule decides and evaluation stops.
  3. The default rule AllowVnetInBound (priority 65000) allows traffic from the VirtualNetwork service tag, which includes peered VNets.
  4. A timeout usually means something (NSG, firewall, routing) silently dropped the packets. Refused means the packet reached the host and nothing was listening on that port.
  5. The private DNS zone was linked only to vnet-app, so vm-shared used public DNS. Peering doesn't share DNS zones.
  6. The account rejects connections arriving over its public endpoint; only private endpoints (and any allowed exceptions) can reach it.

Summary

  • VNets are isolated; peering connects two of them, non-transitively.
  • NSGs are stateful, priority-ordered rule lists with default rules you must know.
  • Use effective routes, effective NSG rules and test-ip-flow instead of guessing.
  • Private endpoints need private DNS that every client VNet can resolve.
  • Once private access works, disable public network access.

Further reading