Secure a Hub-and-Spoke Network on Azure
Build a secured hub-and-spoke network with NSGs, UDRs, and VNet peering.
Introduction
30 Second Summary
Every company that runs workloads in the cloud needs to keep them separated and secure, just like how office buildings have locked floors, security checkpoints, and visitor badges to control who goes where.
In this project, you will build a secure hub-and-spoke network on Azure with isolated Dev and Prod environments, layered security controls, and centralized traffic management.
What You'll Build
A hub-and-spoke network architecture with three Virtual Networks, multiple layers of security controls (NSGs, UDRs, and Azure Virtual Network Manager policies), and test VMs to prove everything works.
By the end of this project, you'll have:
- A working hub-and-spoke network with one hub VNet and two spoke VNets (Dev and Prod), connected via VNet peering with no direct spoke-to-spoke communication.
- Layered security controls deployed across the network: NSGs with least-privilege rules, UDRs forcing spoke traffic through the hub, and a security admin policy blocking unauthorized outbound traffic.
- Validated security controls by testing actual connectivity from VMs deployed in the spoke VNets.
- Deployed Azure Firewall as a centralized egress point with application-level traffic control (Secret Mission).
Are there any prerequisites?
You need an Azure account with an active subscription (free or pay-as-you-go) and Azure CLI installed on macOS. This project directly practices the "Secure networking" domain of the AZ-500 exam, which makes up 20-25% of exam questions.
Set Up Your Azure Environment
Before you can build a hub-and-spoke network, you need a place on Azure to put everything. A resource group acts as a container for all the resources you'll create in this project. In this step, you'll log into Azure CLI, verify your subscription, and create a resource group to hold your entire lab environment.
In This Step, Get Ready To:
- Log into Azure CLI and confirm your subscription.
- Create a resource group to contain all lab resources.
- Verify the resource group is ready to use.
Log Into Azure CLI
Azure CLI lets you manage Azure resources directly from your terminal. You need an authenticated session before you can create anything.
- Open your macOS terminal.
- Run the following command to log in.
az login
A browser window opens and prompts you to sign in with your Azure account. After signing in, the terminal displays your subscription details.
- Verify your account details by running the following command.
az account show --output table
What should I see?
You should see a table with your subscription name, subscription ID, tenant ID, and state. The state should show Enabled.
I see my subscription
You're all set. If you have multiple subscriptions and want to use a specific one, run the command below. Otherwise, skip ahead to the next substep.
az account set --subscription "[[SUBSCRIPTION_ID="your subscription ID"]]"
I see an error
If you see an authentication error, try running az login again. Make sure you complete the browser sign-in before returning to the terminal.
Still having trouble?
Ask for help with your specific error message.
Pro tip: portal alternative
You can also sign in at portal.azure.com and verify your subscription under Subscriptions in the left menu. This project uses CLI as the primary path, but portal instructions are included as alternatives throughout.
Create a Resource Group
A resource group is a logical container that holds all related Azure resources. By placing everything in one group, you can manage, monitor, and delete all your lab resources together.
- Run the following command to create a resource group named rg-hubspoke-lab in the East US region.
az group create \
--name rg-hubspoke-lab \
--location eastus \
--tags Project=HubSpoke Environment=Lab
What does this command do?
- --name rg-hubspoke-lab sets the resource group name. The rg- prefix follows Azure naming conventions for resource groups.
- --location eastus specifies the Azure region where metadata for this group is stored.
- --tags adds metadata labels so you can identify and filter these resources later.
Pro tip: portal alternative
In the Azure portal, search for Resource groups in the top search bar. Click + Create, name it rg-hubspoke-lab, select East US as the region, add your tags, and click Review + create.
Verify the Resource Group
- Run the following command to confirm your resource group exists and is in the correct location.
az group show --name rg-hubspoke-lab --output table
You should see a table showing the resource group name, location as eastus, and provisioning state as Succeeded.
Your Azure environment is ready to go. Next up, you'll build the three virtual networks that form the backbone of your hub-and-spoke architecture.
Build the Hub-and-Spoke Virtual Networks
The foundation of any hub-and-spoke architecture is the network layout itself. You need three separate Virtual Networks (VNets) with carefully planned address spaces that never overlap. In this step, you'll create the hub VNet and two spoke VNets for Dev and Prod workloads.
Getting the CIDR ranges right now prevents painful re-addressing later. Each VNet gets its own /16 block, giving you 65,536 addresses per network. That is far more than this lab needs, but it mirrors how enterprise networks are planned.
In This Step, Get Ready To:
- Create the hub VNet with a management subnet.
- Create two spoke VNets (Dev and Prod) with workload subnets.
- Verify all three VNets have non-overlapping address spaces.
Create the Hub VNet
The hub VNet is the central point of your network. All traffic between spokes will eventually flow through it. You'll give it the 10.0.0.0/16 address space and add a management subnet where centralized services will live.
- Run the following command to create the hub VNet with a management subnet.
az network vnet create \
--name vnet-hub \
--resource-group rg-hubspoke-lab \
--address-prefix 10.0.0.0/16 \
--subnet-name snet-hub-mgmt \
--subnet-prefix 10.0.1.0/24
What does this command do?
This creates a VNet named vnet-hub with a /16 address space (65,536 addresses). Inside it, the snet-hub-mgmt subnet carves out a /24 block (256 addresses) for management resources.
What is CIDR notation?
CIDR (Classless Inter-Domain Routing) notation like 10.0.0.0/16 defines a range of IP addresses. The number after the slash tells you how many bits are fixed. A /16 fixes the first 16 bits, leaving 16 bits for host addresses (65,536 IPs). A /24 fixes 24 bits, leaving room for 256 IPs.
Pro tip: portal alternative
If you prefer the Azure Portal, search for Virtual networks and click + Create. Set the name to vnet-hub, the address space to 10.0.0.0/16, and add a subnet named snet-hub-mgmt with 10.0.1.0/24.
Create the Dev and Prod Spoke VNets
Each spoke VNet represents an isolated workload environment. The Dev spoke gets 10.1.0.0/16 and the Prod spoke gets 10.2.0.0/16. These ranges are deliberately non-overlapping. Overlapping address spaces would prevent VNet peering from working in the next step.
- Run the following command to create the Dev spoke VNet.
az network vnet create \
--name vnet-spoke-dev \
--resource-group rg-hubspoke-lab \
--address-prefix 10.1.0.0/16 \
--subnet-name snet-dev-workload \
--subnet-prefix 10.1.1.0/24
- Run the following command to create the Prod spoke VNet.
az network vnet create \
--name vnet-spoke-prod \
--resource-group rg-hubspoke-lab \
--address-prefix 10.2.0.0/16 \
--subnet-name snet-prod-workload \
--subnet-prefix 10.2.1.0/24
Why these address ranges?
The hub uses 10.0.x.x, Dev uses 10.1.x.x, and Prod uses 10.2.x.x. This pattern follows Azure reference architectures and makes it easy to identify which network a resource belongs to just by looking at its IP address.
Verify Your VNet Setup
- List all VNets in your resource group to confirm all three were created.
az network vnet list \
--resource-group rg-hubspoke-lab \
--output table
What should I see?
You should see a table with three rows: vnet-hub, vnet-spoke-dev, and vnet-spoke-prod. Confirm that each shows a different address prefix: 10.0.0.0/16, 10.1.0.0/16, and 10.2.0.0/16. No two VNets should share the same range.
Not seeing all three VNets?
Double-check that you used rg-hubspoke-lab as the resource group name in every command. A typo in the resource group name creates the VNet in a different group. You can also check the Azure Portal by searching for Virtual networks and filtering by your resource group. Help me troubleshoot my VNet creation.
Your three VNets are in place with clean, non-overlapping address spaces. Right now they are completely isolated from each other. Next up, you'll connect the spokes to the hub using VNet peering.
Peer the Spokes to the Hub
You have three VNets sitting in the same resource group, but right now they are completely isolated from each other. To build the hub-and-spoke topology, you need to connect each spoke to the hub using VNet peering. Peering is non-transitive by default, which means Dev and Prod will never be able to talk to each other directly. This is the network segmentation pattern tested on the AZ-500.
In This Step, Get Ready To:
- Create reciprocal peering links between the hub and the Dev spoke.
- Create reciprocal peering links between the hub and the Prod spoke.
- Verify that all peerings show a Connected status.
Peer the Hub to the Dev Spoke
VNet peering requires two reciprocal links. You create one link from the hub to the spoke, and a second link from the spoke back to the hub. Without both links, the peering status stays as "Initiated" and no traffic flows.
- Run the following command to create the peering link from vnet-hub to vnet-spoke-dev.
az network vnet peering create \
--name peer-hub-to-dev \
--resource-group rg-hubspoke-lab \
--vnet-name vnet-hub \
--remote-vnet vnet-spoke-dev \
--allow-forwarded-traffic \
--allow-vnet-access
- Now create the reciprocal link from vnet-spoke-dev back to vnet-hub.
az network vnet peering create \
--name peer-dev-to-hub \
--resource-group rg-hubspoke-lab \
--vnet-name vnet-spoke-dev \
--remote-vnet vnet-hub \
--allow-forwarded-traffic \
--allow-vnet-access
Why do I need two commands?
VNet peering is a two-way handshake. Each VNet must explicitly agree to the connection by creating its own peering link. If you only create one side, the peering status stays as "Initiated" and no traffic flows.
What does --allow-forwarded-traffic do?
The --allow-forwarded-traffic flag lets the peered VNet accept traffic that did not originate from the remote VNet itself. This is critical for hub-and-spoke scenarios where traffic gets forwarded through a network virtual appliance (NVA) or firewall in the hub.
The --allow-vnet-access flag enables basic communication between the peered VNets.
CLI (Already Done)
If you ran the two CLI commands above, you are all set. The reciprocal peering links for the Dev spoke are created.
Portal Alternative
The Azure Portal can create both peering links at the same time.
- Search for Virtual networks in the portal and select vnet-hub.
- Click Peerings in the left menu, then click + Add.
- Enter peer-hub-to-dev as the peering link name for this VNet.
- Select vnet-spoke-dev as the remote virtual network.
- Enter peer-dev-to-hub as the remote peering link name.
- Ensure Allow virtual network access is enabled on both sides, then click Add.
The portal creates both reciprocal links automatically when you configure both sides in the Add peering dialog.
Peer the Hub to the Prod Spoke
The process is identical for the Prod spoke. You need two more reciprocal links.
- Create the peering link from vnet-hub to vnet-spoke-prod.
az network vnet peering create \
--name peer-hub-to-prod \
--resource-group rg-hubspoke-lab \
--vnet-name vnet-hub \
--remote-vnet vnet-spoke-prod \
--allow-forwarded-traffic \
--allow-vnet-access
- Create the reciprocal link from vnet-spoke-prod back to vnet-hub.
az network vnet peering create \
--name peer-prod-to-hub \
--resource-group rg-hubspoke-lab \
--vnet-name vnet-spoke-prod \
--remote-vnet vnet-hub \
--allow-forwarded-traffic \
--allow-vnet-access
What Is Non-Transitive Peering?
VNet peering is non-transitive by default. Even though Dev is peered to the hub and Prod is peered to the hub, Dev and Prod cannot communicate with each other through the hub automatically. Spoke-to-spoke traffic only flows if you explicitly route it through a network virtual appliance (NVA) in the hub.
This is the foundation of network segmentation in a hub-and-spoke topology. The hub controls all cross-spoke communication.
Verify Peering Status
Both peering connections should now show a status of "Connected." If either side shows "Initiated," the reciprocal link is missing.
- Run the following command to list all peerings on the hub VNet.
az network vnet peering list \
--resource-group rg-hubspoke-lab \
--vnet-name vnet-hub \
--output table
What should I see?
You should see two rows in the table output: peer-hub-to-dev and peer-hub-to-prod. Both should have a PeeringState of Connected.
Peering shows Initiated instead of Connected?
This means the reciprocal peering link is missing. Double-check that you ran both commands for the affected spoke. The second command should use --vnet-name vnet-spoke-dev (or vnet-spoke-prod) with --remote-vnet vnet-hub. Help me fix my peering status.
Your hub-and-spoke topology is now wired up. The spokes can reach the hub, but they cannot reach each other. Next up, you will lock down traffic on every subnet with Network Security Groups.
Lock Down Traffic with Network Security Groups
Your hub-and-spoke network is connected, but right now every subnet accepts all traffic by default. That is the opposite of secure. Network Security Groups (NSGs) act as virtual firewalls at the subnet level, filtering traffic based on rules you define. In this step, you will create NSGs with least-privilege rules and attach them to every subnet in your network.
NSGs are one of the most heavily tested topics on the AZ-500 exam. The exam expects you to know how to write rules with correct priorities, protocols, and source/destination scoping.
In This Step, Get Ready To:
- Create three NSGs, one for each subnet in the hub-and-spoke network.
- Add least-privilege security rules that allow only the traffic you explicitly approve.
- Associate each NSG with its corresponding subnet to activate the rules.
Create Three Network Security Groups
Each subnet needs its own NSG so you can tailor rules to the specific workload running there. The hub management subnet, the Dev workload subnet, and the Prod workload subnet all have different security requirements.
- Run the following commands to create all three NSGs in your resource group.
# Create the hub management subnet NSG
az network nsg create \
--name nsg-hub-mgmt \
--resource-group rg-hubspoke-lab \
--tags Project=HubSpoke Environment=Hub
# Create the Dev workload subnet NSG
az network nsg create \
--name nsg-dev-workload \
--resource-group rg-hubspoke-lab \
--tags Project=HubSpoke Environment=Dev
# Create the Prod workload subnet NSG
az network nsg create \
--name nsg-prod-workload \
--resource-group rg-hubspoke-lab \
--tags Project=HubSpoke Environment=Prod
What do these commands do?
Each az network nsg create command creates an empty NSG with no custom rules yet. The --tags flag labels each NSG with its environment so you can identify them at a glance in the portal.
- Verify all three NSGs were created.
az network nsg list \
--resource-group rg-hubspoke-lab \
--output table
You should see all three NSGs listed: nsg-hub-mgmt, nsg-dev-workload, and nsg-prod-workload.
Add Least-Privilege Security Rules
An empty NSG uses Azure's default rules, which allow all inbound traffic from within the VNet and all outbound traffic to the internet. That is far too permissive. You will add custom rules that only allow the specific traffic your workloads need, then deny everything else.
First, find your public IP address. You will use this to restrict SSH access so only your machine can connect.
- Run the following command to get your current public IP.
curl -s ifconfig.me
Pro tip: save your IP
Copy the IP address that is returned. You will use it in the next commands. If your IP changes later (for example, reconnecting to WiFi), you will need to update these NSG rules or you will be locked out of your VMs.
Now add rules to the Dev workload NSG. You will allow SSH from your IP, allow ICMP pings from the hub VNet, and then deny all other inbound traffic.
- Run the following commands to add rules to nsg-dev-workload. Replace the IP placeholder with your actual public IP.
# Allow SSH only from your public IP
az network nsg rule create \
--nsg-name nsg-dev-workload \
--resource-group rg-hubspoke-lab \
--name AllowSSHFromMyIP \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes [[MY_PUBLIC_IP="your public IP address from curl"]] \
--destination-port-ranges 22
# Allow ICMP ping from the hub VNet
az network nsg rule create \
--nsg-name nsg-dev-workload \
--resource-group rg-hubspoke-lab \
--name AllowICMPFromHub \
--priority 110 \
--direction Inbound \
--access Allow \
--protocol Icmp \
--source-address-prefixes 10.0.0.0/16 \
--destination-port-ranges '*'
# Deny all other inbound traffic
az network nsg rule create \
--nsg-name nsg-dev-workload \
--resource-group rg-hubspoke-lab \
--name DenyAllInbound \
--priority 4000 \
--direction Inbound \
--access Deny \
--protocol '*' \
--source-address-prefixes '*' \
--destination-port-ranges '*'
How do NSG rule priorities work?
Azure evaluates NSG rules from lowest priority number to highest. A rule at priority 100 is checked before a rule at priority 4000. The first matching rule wins. By placing your Allow rules at 100 and 110, they get evaluated before the DenyAll rule at 4000.
Now add rules to the Prod workload NSG. Prod gets the same SSH and deny-all rules, but also an explicit rule blocking all traffic from the Dev CIDR. This enforces spoke isolation at the NSG layer.
- Run the following commands to add rules to nsg-prod-workload. Replace the IP placeholder with your actual public IP.
# Allow SSH only from your public IP
az network nsg rule create \
--nsg-name nsg-prod-workload \
--resource-group rg-hubspoke-lab \
--name AllowSSHFromMyIP \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes [[MY_PUBLIC_IP="your public IP address from curl"]] \
--destination-port-ranges 22
# Allow ICMP ping from the hub VNet
az network nsg rule create \
--nsg-name nsg-prod-workload \
--resource-group rg-hubspoke-lab \
--name AllowICMPFromHub \
--priority 110 \
--direction Inbound \
--access Allow \
--protocol Icmp \
--source-address-prefixes 10.0.0.0/16 \
--destination-port-ranges '*'
# Deny all traffic from the Dev spoke CIDR
az network nsg rule create \
--nsg-name nsg-prod-workload \
--resource-group rg-hubspoke-lab \
--name DenyDevInbound \
--priority 200 \
--direction Inbound \
--access Deny \
--protocol '*' \
--source-address-prefixes 10.1.0.0/16 \
--destination-port-ranges '*'
# Deny all other inbound traffic
az network nsg rule create \
--nsg-name nsg-prod-workload \
--resource-group rg-hubspoke-lab \
--name DenyAllInbound \
--priority 4000 \
--direction Inbound \
--access Deny \
--protocol '*' \
--source-address-prefixes '*' \
--destination-port-ranges '*'
Why explicitly deny Dev traffic to Prod?
The DenyAll rule at priority 4000 would catch Dev traffic anyway. But adding an explicit DenyDevInbound rule at priority 200 makes the intent crystal clear to anyone reading the NSG. In production, explicit deny rules also make auditing easier because they show up as named rules in logs rather than being caught by a generic catch-all.
Getting an error creating rules?
Make sure you replaced the IP placeholder with your actual public IP address (just the numbers, like 203.0.113.42). Also verify the NSG names match exactly. Help me troubleshoot my NSG rule creation error.
Associate NSGs with Their Subnets
Creating an NSG does not automatically protect anything. You need to associate each NSG with its target subnet for the rules to take effect.
- Run the following commands to associate each NSG with its corresponding subnet.
# Associate NSG with the hub management subnet
az network vnet subnet update \
--vnet-name vnet-hub \
--name snet-hub-mgmt \
--resource-group rg-hubspoke-lab \
--nsg nsg-hub-mgmt
# Associate NSG with the Dev workload subnet
az network vnet subnet update \
--vnet-name vnet-spoke-dev \
--name snet-dev-workload \
--resource-group rg-hubspoke-lab \
--nsg nsg-dev-workload
# Associate NSG with the Prod workload subnet
az network vnet subnet update \
--vnet-name vnet-spoke-prod \
--name snet-prod-workload \
--resource-group rg-hubspoke-lab \
--nsg nsg-prod-workload
What does this command do?
The az network vnet subnet update --nsg command links the NSG to the subnet. From this point on, every packet entering or leaving that subnet is evaluated against the NSG's rules.
- Verify the associations by checking one of the subnets.
az network vnet subnet show \
--vnet-name vnet-spoke-dev \
--name snet-dev-workload \
--resource-group rg-hubspoke-lab \
--query 'networkSecurityGroup.id' \
--output tsv
The output should show a resource ID ending in nsg-dev-workload. This confirms the NSG is attached and actively filtering traffic on that subnet.
I see the NSG resource ID
Your NSG is successfully associated with the subnet. Every subnet in your hub-and-spoke network now has a dedicated firewall filtering its traffic.
I see null or an error
If the output is null, the association did not take effect. Double-check that the VNet name, subnet name, and NSG name are all spelled correctly. Help me fix my NSG subnet association.
Every subnet in your network is now protected by its own set of least-privilege rules. Next up, you will add User-Defined Routes to control how traffic flows between the spokes and the hub.
Control Traffic Flow with User-Defined Routes
Your NSGs control which traffic is allowed, but they don't control where that traffic flows. By default, Azure routes traffic between peered VNets directly. That means a packet from Dev could reach Prod without ever passing through the hub. User-Defined Routes (UDRs) override Azure's default routing and force traffic through the path you choose.
In This Step, Get Ready To:
- Create route tables with BGP route propagation disabled for full routing control.
- Add user-defined routes that redirect cross-spoke traffic through the hub.
- Associate each route table with its corresponding spoke subnet.
Create a Route Table for the Dev Spoke
A route table is a collection of routing rules that Azure applies to a subnet. By default, Azure automatically propagates routes learned from BGP (Border Gateway Protocol). Disabling BGP propagation ensures that only your custom routes apply, giving you full control over where traffic goes.
- Run the following command to create a route table for the Dev spoke with BGP propagation disabled.
az network route-table create \
--name rt-dev-to-hub \
--resource-group rg-hubspoke-lab \
--location eastus \
--disable-bgp-route-propagation true \
--tags Project=HubSpoke Environment=Dev
What does this command do?
This creates an empty route table named rt-dev-to-hub. The --disable-bgp-route-propagation true flag prevents Azure from automatically injecting learned routes. Without this flag, Azure could add routes that bypass your custom rules.
Add Routes That Force Traffic Through the Hub
Now you need to tell Azure what to do with traffic that the Dev spoke sends toward the Prod spoke. The route will redirect this traffic to a specific IP address in the hub, using a next-hop type of VirtualAppliance. This is the same next-hop type you would use with a firewall or network virtual appliance (NVA) in production.
- Add a route to the Dev route table that sends Prod-bound traffic to a hub IP address.
az network route-table route create \
--name route-to-prod \
--route-table-name rt-dev-to-hub \
--resource-group rg-hubspoke-lab \
--address-prefix 10.2.0.0/16 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.1.4
What does this route do?
This route matches any traffic headed to 10.2.0.0/16 (the Prod VNet) and redirects it to 10.0.1.4 in the hub's management subnet. Since there is no actual appliance listening at that IP, the traffic gets dropped. This is the intended behavior for this lab.
Why does dropped traffic matter?
The UDR proves that you can override Azure's default routing to prevent direct spoke-to-spoke communication. In production, you would place an Azure Firewall or NVA at that hub IP to inspect and forward the traffic. The secret mission at the end of this project walks you through exactly that.
Now create a second route table for the Prod spoke, with a route pointing Dev-bound traffic to the same hub IP.
- Create the Prod route table.
az network route-table create \
--name rt-prod-to-hub \
--resource-group rg-hubspoke-lab \
--location eastus \
--disable-bgp-route-propagation true \
--tags Project=HubSpoke Environment=Prod
What does this command do?
This is the mirror image of the Dev route table. It creates an empty route table for the Prod spoke with BGP propagation disabled, ready for a custom route.
- Add a route that sends Dev-bound traffic to the hub.
az network route-table route create \
--name route-to-dev \
--route-table-name rt-prod-to-hub \
--resource-group rg-hubspoke-lab \
--address-prefix 10.1.0.0/16 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.1.4
What does this command do?
This route catches any traffic from the Prod spoke headed to 10.1.0.0/16 (the Dev VNet) and redirects it to the same hub IP at 10.0.1.4. Both spokes now have symmetrical routing rules that prevent direct communication.
Associate Route Tables with Spoke Subnets
A route table does nothing until it is associated with a subnet. Once associated, every resource in that subnet follows the custom routes.
- Associate the Dev route table with the Dev workload subnet.
az network vnet subnet update \
--name snet-dev-workload \
--vnet-name vnet-spoke-dev \
--resource-group rg-hubspoke-lab \
--route-table rt-dev-to-hub
- Associate the Prod route table with the Prod workload subnet.
az network vnet subnet update \
--name snet-prod-workload \
--vnet-name vnet-spoke-prod \
--resource-group rg-hubspoke-lab \
--route-table rt-prod-to-hub
- Verify the association by checking the Dev subnet details.
az network vnet subnet show \
--name snet-dev-workload \
--vnet-name vnet-spoke-dev \
--resource-group rg-hubspoke-lab \
--query '{subnet: name, routeTable: routeTable.id}' \
--output table
What should I see?
The output should show the subnet name snet-dev-workload with a routeTable value containing the resource ID of rt-dev-to-hub. If the route table field is empty, the association did not apply. Run the az network vnet subnet update command again.
Pro tip: verify effective routes later
You can see UDRs in action using az network nic show-effective-route-table, but this requires a NIC to exist in the subnet. You will run this command in the next step after deploying test VMs.
Your spoke subnets now have custom routing rules that redirect cross-spoke traffic through the hub. Combined with the NSGs from the previous step, you have two layers of security: rules controlling which traffic is allowed and routes controlling where traffic flows. Next up, you will deploy test VMs and prove that these controls actually work.
Deploy Test VMs and Validate Security Controls
You have built a full hub-and-spoke network with NSGs locking down traffic and UDRs forcing cross-spoke traffic through the hub. Now it is time to prove these controls actually work. In this step, you will deploy lightweight test VMs in each spoke and run connectivity tests to validate that your security rules are doing their job.
In This Step, Get Ready To:
- Deploy a B1s Linux VM in each spoke subnet.
- Test connectivity between the Dev and Prod VMs to confirm spoke isolation.
- Verify effective NSG rules and UDRs on each VM's network interface.
Deploy Test VMs in the Spoke Subnets
Security rules are only useful if they actually block what they should. To test your NSGs and UDRs, you need real compute resources generating traffic inside the network. You will deploy a small Ubuntu VM in each spoke subnet using SSH key authentication.
- Run the following command to create a VM in the Dev spoke subnet.
az vm create \
--resource-group rg-hubspoke-lab \
--name vm-dev-test \
--vnet-name vnet-spoke-dev \
--subnet snet-dev-workload \
--image Ubuntu2204 \
--size Standard_B1s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard \
--output table
What does this command do?
This creates a Standard_B1s Ubuntu VM (about $0.01/hour) inside your Dev spoke subnet. The --generate-ssh-keys flag creates an SSH key pair on your machine if one does not already exist. The public IP lets you SSH in from your local terminal for testing.
- Wait for the VM to finish provisioning. This takes 2-3 minutes.
- Note the publicIpAddress from the output. You will need it to SSH into the Dev VM.
- Run the following command to create a VM in the Prod spoke subnet.
az vm create \
--resource-group rg-hubspoke-lab \
--name vm-prod-test \
--vnet-name vnet-spoke-prod \
--subnet snet-prod-workload \
--image Ubuntu2204 \
--size Standard_B1s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard \
--output table
- Note the publicIpAddress and the privateIpAddress for the Prod VM from the output. The private IP will be in the 10.2.1.x range.
Pro tip: portal alternative
You can also create VMs in the portal. Search for Virtual machines, click + Create, choose the B1s size with an Ubuntu image, select SSH key authentication, and place each VM in the correct VNet and subnet.
VM creation failing?
If you see a quota or capacity error, your subscription may not have enough vCPU quota for the East US region. Try a different region or request a quota increase in the portal under Subscriptions > Usage + quotas. Help me troubleshoot my VM creation error.
Test Connectivity Between the Spoke VMs
This is the moment of truth. If your security controls are working, the Dev VM should not be able to reach the Prod VM. The UDR sends cross-spoke traffic to a hub IP where no appliance exists, so the traffic gets dropped. The Prod NSG also explicitly denies traffic from the Dev CIDR. Two layers of defense, both blocking the same traffic.
- SSH into the Dev VM using its public IP address.
ssh azureuser@[[DEV_VM_PUBLIC_IP="your Dev VM public IP address"]]
- From inside the Dev VM, try to ping the Prod VM's private IP address.
ping -c 4 [[PROD_VM_PRIVATE_IP="your Prod VM private IP, e.g. 10.2.1.4"]]
What should I see?
The ping should fail with 100% packet loss or time out completely. This confirms that your spoke isolation is working. The UDR redirects this traffic to a hub IP (10.0.1.4) where no appliance exists to forward it. The Prod NSG also denies inbound traffic from the Dev CIDR (10.1.0.0/16). Either control alone would block this traffic. Together, they provide defense in depth.
- Test outbound internet access from the Dev VM.
curl ifconfig.me
This should return a public IP address, confirming that the VM has outbound internet access. You will block this in the next step using Azure Virtual Network Manager.
- Type exit to disconnect from the Dev VM.
SSH connection timed out?
Your NSG rule allows SSH only from the public IP you configured in Step 4. If your IP address has changed (for example, by reconnecting to WiFi), you will be locked out. Run curl -s ifconfig.me from your local terminal to check your current IP, then update the NSG rule's source address. Help me fix my SSH connection to the Azure VM.
Verify Effective Security Rules and Routes
Azure lets you inspect the effective rules on any network interface. This shows you exactly which NSG rules and routes are applied after all layers are combined. It is one of the most useful troubleshooting tools for network security.
- First, find the NIC name for the Dev VM.
az vm show \
--resource-group rg-hubspoke-lab \
--name vm-dev-test \
--query 'networkProfile.networkInterfaces[0].id' \
--output tsv
The output is the full resource ID. The NIC name is the last segment after the final / (it will look something like vm-dev-testVMNic).
- Check the effective NSG rules on the Dev VM's NIC.
az network nic list-effective-nsg \
--resource-group rg-hubspoke-lab \
--name [[DEV_NIC_NAME="your Dev VM NIC name, e.g. vm-dev-testVMNic"]]
What should I see?
The output shows all effective security rules on the NIC, including your custom rules (SSH allow, ICMP from hub, deny-all) and Azure's default rules. Your custom rules should appear with the priority numbers you set in Step 4. Confirm that the deny-all rule has a higher priority number (lower precedence) than your allow rules.
- Check the effective routes on the Dev VM's NIC to see the UDR in action.
az network nic show-effective-route-table \
--resource-group rg-hubspoke-lab \
--name [[DEV_NIC_NAME="your Dev VM NIC name, e.g. vm-dev-testVMNic"]] \
--output table
What should I see?
You should see your custom route for 10.2.0.0/16 (the Prod CIDR) with a next-hop type of VirtualAppliance and a next-hop IP of 10.0.1.4. This confirms that the UDR is overriding Azure's default routing for cross-spoke traffic. The source column shows "User" for your custom route, distinguishing it from the "Default" system routes.
Your security controls are validated. The Dev and Prod spokes are fully isolated from each other, with both NSG rules and UDRs enforcing the separation. Next up, you will add a centralized policy layer using Azure Virtual Network Manager that can override even NSG rules to block outbound internet traffic from the spokes.
Enforce Centralized Policy with Azure Virtual Network Manager
Your NSGs and UDRs are doing their job at the subnet level. But what happens when a team member accidentally adds an NSG rule that allows outbound internet access? Azure Virtual Network Manager (AVNM) solves this by creating security admin policies that sit above NSGs and cannot be overridden by individual network administrators. In this step, you'll create a centralized policy that blocks outbound HTTP/HTTPS traffic from both spoke VNets.
In This Step, Get Ready To:
- Create an Azure Virtual Network Manager instance with security admin features enabled.
- Build a security admin configuration that blocks outbound HTTP/HTTPS from the spoke VNets.
- Deploy the policy and validate that it overrides NSG-level rules.
Create an Azure Virtual Network Manager Instance
AVNM is the control plane that lets you manage network policies across multiple VNets from a single place. CLI support for AVNM is limited, so you'll use the Azure Portal for this step.
- In the Azure Portal, search for Network managers in the top search bar.
- Click + Create.
- Enter vnm-hubspoke-lab as the name.
- Select your subscription as the Scope.
- Under Features, enable both Connectivity and Security admin.
- Click Review + create, then Create.
Why enable both features?
AVNM offers two feature sets. Connectivity manages VNet topology (mesh, hub-and-spoke). Security admin creates override-level security rules that sit above NSGs. You must enable these features at creation time because they cannot be added later.
Now you need to create a network group that tells AVNM which VNets to manage.
- Open your newly created vnm-hubspoke-lab network manager.
- In the left menu, select Network groups.
- Click + Create and name the group ng-spokes.
- Open ng-spokes and click Add members.
- Select both vnet-spoke-dev and vnet-spoke-prod as members, then click Add.
Create a Security Admin Configuration
A security admin configuration is a collection of rule sets that AVNM enforces across your network groups. The rules you define here cannot be overridden by NSG rules on individual subnets. This is the centralized control that makes AVNM powerful for enterprise governance.
- In your network manager, select Configurations from the left menu.
- Click + Create and choose Security admin configuration.
- Name it sac-deny-internet.
- Click + Add a rule collection.
- Name the rule collection and set the target network group to ng-spokes.
Now add the deny rule inside the rule collection.
- Click + Add a rule.
- Set the rule name to DENY_INTERNET.
- Set Action to Deny.
- Set Direction to Outbound.
- Set Protocol to TCP.
- Set Destination port to 80,443.
- Set Priority to 1.
- Click Add to save the rule, then save the rule collection and configuration.
What makes security admin rules different from NSG rules?
NSG rules can be modified by anyone with permissions on that specific NSG. Security admin rules are evaluated before NSGs and cannot be overridden at the subnet level. Even if someone adds an NSG rule allowing port 80 outbound, the AVNM deny rule still blocks the traffic.
This is the "Manage virtual networks by using Azure Virtual Network Manager" objective on the AZ-500 exam.
Deploy the Configuration and Validate
Creating a configuration does not apply it. You need to explicitly deploy it to a target region for the rules to take effect.
- In your network manager, select Deployments from the left menu.
- Click Deploy configurations.
- Select Security admin as the configuration type.
- Select sac-deny-internet as the configuration.
- Choose East US as the target region.
- Click Deploy.
Pro tip: deployment takes a few minutes
AVNM security admin deployments can take a few minutes to propagate to all VNets in the target region. Wait for the deployment status to show as successful before testing.
Once the deployment completes, test that the security admin rule is actually blocking outbound HTTP/HTTPS traffic.
- SSH into your Dev VM using its public IP.
ssh azureuser@[[DEV_VM_IP="your Dev VM public IP address"]]
- From inside the Dev VM, attempt to reach an external website over HTTP.
curl http://example.com
This request should hang or fail. The AVNM security admin rule is blocking outbound TCP traffic on ports 80 and 443 before the request ever reaches the NSG layer.
What should I see?
The curl command should time out or return a connection error. If you previously ran curl ifconfig.me successfully in Step 6, this confirms the AVNM rule is now blocking traffic that was previously allowed.
- Verify the rule in the portal by navigating to the Dev VM's network interface.
- Select Effective security rules under the Help section.
You should see the DENY_INTERNET rule listed alongside your NSG rules. The security admin rule appears with a different source, confirming it is enforced at the AVNM level above the NSG.
Still able to reach external websites?
If curl http://example.com still returns a response, the deployment may not have finished propagating. Wait a few more minutes and try again. Also confirm that the deployment status shows as successful in the Deployments section of your network manager. Help me troubleshoot my AVNM deployment.
You now have three layers of network security working together: NSGs controlling subnet-level traffic, UDRs forcing routing through the hub, and AVNM security admin policies enforcing organization-wide rules that no one can bypass. Next up, tackle the secret mission to add Azure Firewall as a centralized egress point for production-grade traffic inspection.
Secret mission
Add Azure Firewall as the Centralized Egress Point
Your hub-and-spoke network has layered security controls, but the UDRs are dropping traffic into a void. There is no actual appliance inspecting and forwarding packets in the hub. In production, Azure Firewall sits at the center of the hub, inspecting every packet that leaves your spokes. Deploy one and see the difference between "deny by absence" and "deny by policy."
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The VNets, NSGs, UDRs, peering, and AVNM instance are all free, but the two B1s VMs cost about $0.02/hour combined and any public IPs add a small charge.
Cost warning
If you completed the secret mission, make sure you have already deleted the Azure Firewall and its public IP. Azure Firewall costs approximately $1.25/hour. Even a few hours of forgetting about it can add up quickly.
Resources you used:
- Resource group rg-hubspoke-lab (contains all resources below).
- 3 Virtual Networks: vnet-hub, vnet-spoke-dev, vnet-spoke-prod (free).
- VNet peering links between hub and spokes (free).
- 3 Network Security Groups: nsg-hub-mgmt, nsg-dev-workload, nsg-prod-workload (free).
- 2 Route tables with user-defined routes (free).
- Azure Virtual Network Manager instance vnm-hubspoke-lab (free).
- 2 B1s Linux VMs with public IPs (~$0.02/hour combined).
- Azure Firewall and public IP (secret mission only, ~$1.25/hour).
Keep everything running
No action needed. Choose this if you are still actively experimenting with the network and plan to continue shortly.
- Your two B1s VMs will continue running at ~$0.0104/hour each.
- All networking resources (VNets, NSGs, UDRs, peering, AVNM) remain free.
- At this rate, keeping everything running costs roughly $15/month total for the VMs.
Pause - I'll come back to this later
Deallocate the VMs to stop compute charges while keeping your entire network setup intact. The VNets, NSGs, UDRs, peering, and AVNM instance are all free, so there is no cost to keeping them.
- Deallocate both VMs by running az vm deallocate --resource-group rg-hubspoke-lab --name <vm-name> for each VM.
- Deallocated VMs stop all compute billing. The disks remain but cost very little.
- When you are ready to resume, start the VMs with az vm start --resource-group rg-hubspoke-lab --name <vm-name>.
- Note that your VMs may receive new public IPs when restarted. Update your NSG SSH rules with your current public IP before reconnecting.
Delete - I don't want to use this again
Remove all project resources in one command. This is the recommended option if you have finished the project.
- Run az group delete --name rg-hubspoke-lab --yes --no-wait to delete the resource group and everything inside it.
- The --no-wait flag returns control to your terminal immediately while Azure deletes resources in the background.
- This deletes all VNets, NSGs, UDRs, peering links, route tables, VMs, public IPs, and the AVNM instance.
- If you deployed Azure Firewall during the secret mission, confirm it is included in the resource group deletion. You can verify with az group show --name rg-hubspoke-lab after a few minutes. A "not found" error confirms everything is gone.
Mission accomplished
That's a Wrap!
Nice work! You just built an enterprise-grade hub-and-spoke network on Azure with layered security controls that map directly to the AZ-500 "Secure networking" domain.
You've learned how to:
- Design a hub-and-spoke network topology with non-overlapping CIDR ranges, VNet peering, and spoke isolation that prevents direct Dev-to-Prod communication.
- Layer multiple security controls across the network, including NSGs with least-privilege rules, UDRs forcing traffic through the hub, and AVNM security admin policies that override NSG rules.
- Validate security controls by testing actual connectivity from deployed VMs, verifying effective security rules and effective routes on each VM's NIC.
- Deploy Azure Firewall as a centralized egress point in the hub with application rules that allow only approved outbound destinations.
Ready to quiz yourself?