Troubleshoot Secure VNet Peering
Deploy private Azure VNets with Bicep and repair a one-sided peering.
Introduction
30 Second Summary
A bridge that stops short of the opposite bank is useless, even if one end looks finished. A computer connection can hide the same gap behind a screen that says everything worked.
In this project, you will build two private Azure Virtual Networks using Bicep. You will troubleshoot an incomplete VNet peering link until private traffic works in both directions.
What You'll Build
Your demo replays the same private ping before and after the repair, showing the peering state move from Initiated to Connected as traffic begins crossing between both private virtual machines.
By the end of this project, you'll have:
- A private networking lab you can deploy with two Ubuntu virtual machines that support Azure VM Run Command through NAT Gateway while keeping public IP addresses off both machines.
- Clear failure evidence showing peer-hub-to-spoke in Initiated while private traffic to 10.31.1.4 loses packets.
- Verified repair evidence where both peerings reach Connected before private pings succeed in both directions.
- Secret Mission: Narrow each NSG rule for ICMP to the exact remote VM address without breaking either ping.
Are there any prerequisites?
You need completed Projects 01 and 02 plus an Azure subscription with Owner permissions and available Standard_B1s quota in eastus. This paid lab uses up to $1 of Azure credit, while the final cleanup removes every lab resource.
Before We Start
Before the hands-on work begins, commit to the lab's failure hypothesis: private ICMP from vm-project03-hub at 10.30.1.4 to vm-project03-spoke at 10.31.1.4 fails before reciprocal VNet peering exists even though Azure VM Run Command reaches the hub VM through its management path. The repair is proven when both peerings show Connected and bidirectional four-packet pings report 0% packet loss.
Prepare the Project and Verify the Toolchain
This paid Microsoft Azure lab should begin only after you confirm the active subscription and target region. A clean resource-group name also prevents an earlier deployment from hiding the failure you need to observe.
You will prepare the files with PowerShell and Visual Studio Code. You will then verify the Azure CLI, Bicep, and Git before compiling the template locally.
In this step, get ready to:
- Create the Project 03 folder with its deployment, cleanup, and evidence files.
- Verify the local tools, Azure subscription, target region, VM size, and clean resource-group name.
- Review the Bicep template and compile it without creating a generated JSON file.
Create the project files
The project folder separates this lab from Projects 01 and 02. Its files define the infrastructure, provide deterministic cleanup, and hold the evidence you collect later.
Keep Credentials Out of Git
The template declares adminPassword as a secure parameter. You will enter one temporary password when Azure prompts during deployment, so this folder needs no password file or parameters file.
- Return to the PowerShell window at the root of your connected repository from Projects 01 and 02.
- Create the project-03-vnet-peering folder and move into it by running:
New-Item -Path 'project-03-vnet-peering' -ItemType Directory
Set-Location -Path 'project-03-vnet-peering'
What Do These Commands Do?
- New-Item creates the named directory inside the repository.
- Set-Location makes that new directory the active PowerShell location.
You will see the new directory in the command output. Your PowerShell prompt now ends with project-03-vnet-peering.
- Create the three project files by running:
New-Item -Path '.\main.bicep', '.\cleanup.ps1', '.\README.md' -ItemType File
What Does This Command Do?
PowerShell creates the empty main.bicep, cleanup.ps1, and README.md files together. Keeping the files in one folder gives the lab a self-contained deployment and cleanup process.
The command output lists all three files. That visible list confirms the project shell now exists.
- Open the current project folder in Visual Studio Code by running:
code .
What Does This Command Do?
The code . command opens the current PowerShell directory as a Visual Studio Code workspace. The Explorer sidebar should list the three files you just created.
- Select main.bicep in the Explorer sidebar.
- Paste the complete main.bicep source from the double-check tab below.
- Save main.bicep by pressing Ctrl+S.
- Select cleanup.ps1 in the Explorer sidebar.
- Paste the complete cleanup.ps1 source from the double-check tab below.
- Save cleanup.ps1 by pressing Ctrl+S.
How the Two Files Work Together
The Bicep template declares both private networks and the switchable reciprocal peering. The PowerShell cleanup script deletes the complete resource group synchronously and confirms its absence before reporting success.
- Select README.md in the Explorer sidebar.
- Add the initial evidence statement by copying this line:
Initial Project 03 evidence file exists with no password and no deployment evidence yet.
Why Start With This Statement?
The sentence records the exact evidence state before deployment. It also makes the absence of stored credentials explicit.
- Save README.md by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Great. Confirm that Visual Studio Code lists main.bicep, cleanup.ps1, and README.md with no unsaved-change indicators.
ⓧ I'd like to double check the full code
Compare your complete main.bicep with this source. Every identifier and value must match.
@description('Azure region for every resource in the lab.')
param location string = 'eastus'
@description('Small VM size used for the short-lived connectivity test.')
param vmSize string = 'Standard_B1s'
@description('Local administrator username for both Linux VMs.')
param adminUsername string = 'azureops'
@secure()
@description('Temporary password for both Linux VMs. Never store this value in source control.')
param adminPassword string
@description('Deploy the reciprocal spoke-to-hub peering.')
param completePeering bool = true
@description('Restrict ICMP to the exact remote VM private address instead of the remote VNet CIDR.')
param restrictIcmpToVm bool = false
var hubVnetName = 'vnet-project03-hub'
var spokeVnetName = 'vnet-project03-spoke'
var subnetName = 'snet-workload'
var hubNsgName = 'nsg-project03-hub'
var spokeNsgName = 'nsg-project03-spoke'
var hubNatPublicIpName = 'pip-project03-hub-nat'
var spokeNatPublicIpName = 'pip-project03-spoke-nat'
var hubNatGatewayName = 'nat-project03-hub'
var spokeNatGatewayName = 'nat-project03-spoke'
var hubNicName = 'nic-project03-hub'
var spokeNicName = 'nic-project03-spoke'
var hubVmName = 'vm-project03-hub'
var spokeVmName = 'vm-project03-spoke'
var hubVnetPrefix = '10.30.0.0/16'
var spokeVnetPrefix = '10.31.0.0/16'
var hubSubnetPrefix = '10.30.1.0/24'
var spokeSubnetPrefix = '10.31.1.0/24'
var hubVmPrivateIp = '10.30.1.4'
var spokeVmPrivateIp = '10.31.1.4'
var hubAllowedIcmpSource = restrictIcmpToVm ? spokeVmPrivateIp : spokeVnetPrefix
var spokeAllowedIcmpSource = restrictIcmpToVm ? hubVmPrivateIp : hubVnetPrefix
var resourceTags = {
project: 'az104-project03'
environment: 'lab'
managedBy: 'bicep'
}
resource hubNsg 'Microsoft.Network/networkSecurityGroups@2025-01-01' = {
name: hubNsgName
location: location
tags: resourceTags
properties: {
securityRules: [
{
name: 'Allow-ICMP-From-Peer'
properties: {
priority: 100
protocol: 'Icmp'
access: 'Allow'
direction: 'Inbound'
sourceAddressPrefix: hubAllowedIcmpSource
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
{
name: 'Deny-All-Inbound'
properties: {
priority: 4096
protocol: '*'
access: 'Deny'
direction: 'Inbound'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
]
}
}
resource spokeNsg 'Microsoft.Network/networkSecurityGroups@2025-01-01' = {
name: spokeNsgName
location: location
tags: resourceTags
properties: {
securityRules: [
{
name: 'Allow-ICMP-From-Peer'
properties: {
priority: 100
protocol: 'Icmp'
access: 'Allow'
direction: 'Inbound'
sourceAddressPrefix: spokeAllowedIcmpSource
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
{
name: 'Deny-All-Inbound'
properties: {
priority: 4096
protocol: '*'
access: 'Deny'
direction: 'Inbound'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
]
}
}
resource hubNatPublicIp 'Microsoft.Network/publicIPAddresses@2025-01-01' = {
name: hubNatPublicIpName
location: location
tags: resourceTags
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
idleTimeoutInMinutes: 4
}
}
resource spokeNatPublicIp 'Microsoft.Network/publicIPAddresses@2025-01-01' = {
name: spokeNatPublicIpName
location: location
tags: resourceTags
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
idleTimeoutInMinutes: 4
}
}
resource hubNatGateway 'Microsoft.Network/natGateways@2025-01-01' = {
name: hubNatGatewayName
location: location
tags: resourceTags
sku: {
name: 'Standard'
}
properties: {
idleTimeoutInMinutes: 4
publicIpAddresses: [
{
id: hubNatPublicIp.id
}
]
}
}
resource spokeNatGateway 'Microsoft.Network/natGateways@2025-01-01' = {
name: spokeNatGatewayName
location: location
tags: resourceTags
sku: {
name: 'Standard'
}
properties: {
idleTimeoutInMinutes: 4
publicIpAddresses: [
{
id: spokeNatPublicIp.id
}
]
}
}
resource hubVnet 'Microsoft.Network/virtualNetworks@2025-01-01' = {
name: hubVnetName
location: location
tags: resourceTags
properties: {
addressSpace: {
addressPrefixes: [
hubVnetPrefix
]
}
}
}
resource spokeVnet 'Microsoft.Network/virtualNetworks@2025-01-01' = {
name: spokeVnetName
location: location
tags: resourceTags
properties: {
addressSpace: {
addressPrefixes: [
spokeVnetPrefix
]
}
}
}
resource hubSubnet 'Microsoft.Network/virtualNetworks/subnets@2025-01-01' = {
parent: hubVnet
name: subnetName
properties: {
addressPrefix: hubSubnetPrefix
defaultOutboundAccess: false
natGateway: {
id: hubNatGateway.id
}
networkSecurityGroup: {
id: hubNsg.id
}
}
}
resource spokeSubnet 'Microsoft.Network/virtualNetworks/subnets@2025-01-01' = {
parent: spokeVnet
name: subnetName
properties: {
addressPrefix: spokeSubnetPrefix
defaultOutboundAccess: false
natGateway: {
id: spokeNatGateway.id
}
networkSecurityGroup: {
id: spokeNsg.id
}
}
}
resource hubNic 'Microsoft.Network/networkInterfaces@2025-01-01' = {
name: hubNicName
location: location
tags: resourceTags
properties: {
enableAcceleratedNetworking: false
enableIPForwarding: false
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
primary: true
privateIPAddress: hubVmPrivateIp
privateIPAddressVersion: 'IPv4'
privateIPAllocationMethod: 'Static'
subnet: {
id: hubSubnet.id
}
}
}
]
}
}
resource spokeNic 'Microsoft.Network/networkInterfaces@2025-01-01' = {
name: spokeNicName
location: location
tags: resourceTags
properties: {
enableAcceleratedNetworking: false
enableIPForwarding: false
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
primary: true
privateIPAddress: spokeVmPrivateIp
privateIPAddressVersion: 'IPv4'
privateIPAllocationMethod: 'Static'
subnet: {
id: spokeSubnet.id
}
}
}
]
}
}
resource hubVm 'Microsoft.Compute/virtualMachines@2024-03-01' = {
name: hubVmName
location: location
tags: resourceTags
properties: {
hardwareProfile: {
vmSize: vmSize
}
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
diskSizeGB: 30
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
osProfile: {
computerName: hubVmName
adminUsername: adminUsername
adminPassword: adminPassword
allowExtensionOperations: true
linuxConfiguration: {
disablePasswordAuthentication: false
provisionVMAgent: true
}
}
networkProfile: {
networkInterfaces: [
{
id: hubNic.id
properties: {
primary: true
}
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: false
}
}
}
}
resource spokeVm 'Microsoft.Compute/virtualMachines@2024-03-01' = {
name: spokeVmName
location: location
tags: resourceTags
properties: {
hardwareProfile: {
vmSize: vmSize
}
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
diskSizeGB: 30
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
osProfile: {
computerName: spokeVmName
adminUsername: adminUsername
adminPassword: adminPassword
allowExtensionOperations: true
linuxConfiguration: {
disablePasswordAuthentication: false
provisionVMAgent: true
}
}
networkProfile: {
networkInterfaces: [
{
id: spokeNic.id
properties: {
primary: true
}
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: false
}
}
}
}
resource hubToSpokePeering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2025-01-01' = {
parent: hubVnet
name: 'peer-hub-to-spoke'
properties: {
allowVirtualNetworkAccess: true
allowForwardedTraffic: false
allowGatewayTransit: false
useRemoteGateways: false
remoteVirtualNetwork: {
id: spokeVnet.id
}
}
}
resource spokeToHubPeering 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2025-01-01' = if (completePeering) {
parent: spokeVnet
name: 'peer-spoke-to-hub'
properties: {
allowVirtualNetworkAccess: true
allowForwardedTraffic: false
allowGatewayTransit: false
useRemoteGateways: false
remoteVirtualNetwork: {
id: hubVnet.id
}
}
}
output hubPrivateIp string = hubVmPrivateIp
output spokePrivateIp string = spokeVmPrivateIp
output reciprocalPeeringDeployed bool = completePeering
output icmpRestrictedToVm bool = restrictIcmpToVm
What Does This Template Define?
The template defines two non-overlapping VNets with child subnets, subnet-level NSGs, explicit outbound NAT, private NICs, and Ubuntu VMs. Its parameters let later deployments switch reciprocal peering and exact-host ICMP restrictions without changing the template structure.
Compare your complete cleanup.ps1 with this source. The script must wait for resource-group deletion before checking the final state.
param(
[string]$ResourceGroupName = 'rg-az104-project03'
)
$exists = (az group exists --name $ResourceGroupName).Trim()
if ($LASTEXITCODE -ne 0) {
throw "Could not check resource group '$ResourceGroupName'."
}
if ($exists -eq 'false') {
Write-Host "Resource group '$ResourceGroupName' is already absent."
Write-Host 'false'
exit 0
}
Write-Host "Deleting resource group '$ResourceGroupName' and waiting for completion..."
az group delete --name $ResourceGroupName --yes
if ($LASTEXITCODE -ne 0) {
throw "Azure CLI could not delete resource group '$ResourceGroupName'."
}
$existsAfterDelete = (az group exists --name $ResourceGroupName).Trim()
if ($LASTEXITCODE -ne 0) {
throw "Could not verify deletion of resource group '$ResourceGroupName'."
}
if ($existsAfterDelete -ne 'false') {
throw "Cleanup is incomplete. Resource group '$ResourceGroupName' still exists."
}
Write-Host "Cleanup verified for '$ResourceGroupName'."
Write-Host $existsAfterDelete
What Does This Script Define?
The script checks whether rg-az104-project03 exists before deleting it. After deletion completes, it checks again and only finishes when Azure returns false.
Compare your complete README.md with this initial evidence state.
Initial Project 03 evidence file exists with no password and no deployment evidence yet.
What Does This Evidence Show?
The README confirms that the project exists before any Azure deployment evidence is collected. It contains no temporary VM password.
Files Missing or Unsaved?
- Check that the Visual Studio Code window title includes project-03-vnet-peering.
- Check that each file appears directly beneath the folder in the Explorer sidebar.
- Save any file showing a dot on its editor tab by pressing Ctrl+S.
Still stuck? Help me verify my Project 03 files in Visual Studio Code.
Verify the toolchain and Azure context
The deployment relies on a CLI feature introduced in Azure CLI 2.76.0. Checking the version first prevents an unsupported validation flag from interrupting the paid lab later.
- Return to the PowerShell window from earlier.
- Check the installed Azure CLI version by running:
az --version
What Should You Check?
Find the Azure CLI version near the start of the output. This project requires 2.76.0 or later because the deployment preview uses the validation-level option.
Choose the tab that matches your output.
✔️ I see version 2.76.0 or later
Your Azure CLI supports the commands used throughout this project. Continue with the remaining checks below.
ⓧ I see an older version
Your installed Azure CLI needs an update before this project can use the required deployment validation option.
- Update Azure CLI to the latest version by running:
az upgrade
What Does This Command Do?
The command updates Azure CLI to the latest supported version. It also updates installed Azure CLI extensions by default.
- Wait for the update to finish before running another Azure CLI command.
- Close the current PowerShell window after the update completes.
- Open a new PowerShell terminal from the Terminal menu in Visual Studio Code.
- Run the version check again and confirm the result is 2.76.0 or later.
ⓧ Command not found
Azure CLI is not installed or its executable is missing from your Windows path. WinGet can install the official package.
- Install the latest Azure CLI package by running:
winget install --exact --id Microsoft.AzureCLI
What Does This Command Do?
WinGet installs the official Microsoft Azure CLI package. The exact package identifier prevents a similarly named package from being selected.
- Approve the Windows permission request if one appears.
- Close the current PowerShell window after installation completes.
- Open a new PowerShell terminal from the Terminal menu in Visual Studio Code.
- Run the version check again and confirm the result is 2.76.0 or later.
The remaining checks prove that Bicep and Git respond locally. They also confirm the exact Azure subscription and deployment prerequisites before any paid resource exists.
- Verify Bicep, Git, the active subscription, East US, the VM size, and the resource-group name by running:
az bicep version
git --version
az account show --output table
az account list-locations --query "[?name=='eastus'].{Name:name,DisplayName:displayName}" --output table
az vm list-skus --location eastus --size Standard_B1s --output table
az group exists --name rg-az104-project03
What Do These Checks Prove?
- The Bicep command prints an installed compiler version.
- The Git command prints an installed Git version.
- The account table identifies the active Azure subscription.
- The location table lists eastus as East US.
- The SKU table lists Standard_B1s without a subscription restriction.
- The final command prints false when the lab resource group is absent.
Check that the subscription table names the intended subscription. The final line should read false because this lab needs a fresh resource group for its designed peering failure.
Does a Prerequisite Check Fail?
- Stop before deployment if the account table shows the wrong subscription.
- Stop if East US is absent from the location output.
- Stop if the Standard_B1s table is empty or shows a subscription restriction.
- Stop if the resource-group check returns true because an earlier lab deployment still exists.
Need help interpreting the output? Help me diagnose my Project 03 Azure prerequisite checks.
That is the risk check complete. Your local tools and Azure context are ready without creating a billable resource.
Review and compile the Bicep template
A local Bicep build catches template errors before Azure receives a deployment request. The stdout option sends the compiled result through the terminal pipeline, so the build does not leave a main.json file in your repository.
- Switch back to main.bicep in Visual Studio Code.
- Confirm the parameters are location, vmSize, adminUsername, adminPassword, completePeering, and restrictIcmpToVm.
- Confirm the hub address space is 10.30.0.0/16.
- Confirm the spoke address space is 10.31.0.0/16.
- Confirm the private VM addresses are 10.30.1.4 and 10.31.1.4.
- Confirm the template defines separate NSGs, NAT gateways, NICs, VMs, and peering resources for the hub and spoke.
- Return to the PowerShell window in project-03-vnet-peering.
Before you run the final check, do you expect the compiler to report a diagnostic or return silently?
- Compile the template and recheck the clean resource-group name by running:
az bicep build --file .\main.bicep --stdout | Out-Null
az group exists --name rg-az104-project03
What Does This Check Do?
The first command compiles main.bicep and discards the generated stdout stream after validation. The second command confirms that rg-az104-project03 is still absent.
The build returns to the prompt without diagnostics. The resource-group check prints false.
- Check the Visual Studio Code Explorer sidebar for a generated main.json file.
You should still see only main.bicep, cleanup.ps1, and README.md in the project folder. The missing main.json confirms that compilation stayed clean.
Seeing a Build Diagnostic?
- Compare main.bicep with the full source in the double-check tab.
- Check for a missing brace near the line number shown by the compiler.
- Check that every resource declaration uses the exact API version shown in the reference file.
Still blocked? Help me diagnose my Bicep build output.
- Return PowerShell to the repository root by running:
Set-Location -Path ..
What Does This Command Do?
PowerShell moves up one directory from project-03-vnet-peering to the connected repository root. The project folder remains ready for the deployment step.
Your template compiles, the intended Azure context is confirmed, and the resource-group name is clean. Next, you will deploy the two private networks with only one half of the peering so the designed failure can happen.
Deploy the Incomplete Private Network
Your Bicep template already compiles locally. Your PowerShell session remains signed in to the intended Microsoft Azure subscription.
This step turns that verified template into a working private network lab. The deployment intentionally leaves the reciprocal side of the VNet peering absent so the topology is ready for the next connectivity test.
In this step, get ready to:
- Create the Project 03 resource group in East US.
- Preview the incomplete network topology with Provider-level validation.
- Deploy the private network resources with one-sided peering.
Create the resource group and preview the topology
The resource group gives every lab resource one deployment boundary. The what-if preview then shows the proposed changes before Azure creates the paid infrastructure.
- Switch the PowerShell session from earlier into the project-03-vnet-peering folder by running this command:
Set-Location .\project-03-vnet-peering
What Does This Command Do?
The Set-Location command changes the current PowerShell location to the folder containing main.bicep. This lets the deployment commands resolve the template through its relative path.
Your PowerShell prompt should now end with project-03-vnet-peering.
- Create the resource group by defining its values and running this command:
$ResourceGroup = 'rg-az104-project03'
$Location = 'eastus'
az group create --name $ResourceGroup --location $Location
What Does This Command Do?
- The $ResourceGroup variable holds the lab resource-group name.
- The $Location variable keeps every regional resource in eastus.
- The group creation command creates the boundary that contains the entire lab.
Good progress. Azure returns the new group details with its location set to eastus.
Resource Group Creation Failed?
- Check that $ResourceGroup still contains rg-az104-project03.
- Confirm that your PowerShell session is still signed in to the intended subscription.
- Retry the command after correcting the subscription context or variable value.
Still blocked? Help me troubleshoot why Azure CLI cannot create this resource group.
Credential prompts can feel risky. This password is entered interactively so it never needs to appear in main.bicep. It also stays out of README.md.
- Choose one temporary password containing 12 to 72 characters across at least three Azure complexity categories.
- Keep the password out of every project file.
- Preview the incomplete topology by running this command:
az deployment group what-if `
--resource-group $ResourceGroup `
--template-file .\main.bicep `
--parameters completePeering=false restrictIcmpToVm=false `
--validation-level Provider
What Does This Preview Do?
- The what-if operation evaluates the resource changes without applying them.
- The completePeering=false parameter keeps the spoke-to-hub peering out of this deployment.
- The restrictIcmpToVm=false parameter allows ICMP from each complete remote VNet address range.
- The --validation-level Provider option asks Azure resource providers to validate the planned deployment.
- Enter the temporary password when Azure CLI prompts for adminPassword.
- Confirm that the preview includes two virtual networks with one child subnet in each network.
- Confirm that the preview includes two network security groups with ICMP allow rules.
- Confirm that the preview includes two NAT gateways with two static public IP resources.
- Confirm that the preview includes two private Ubuntu virtual machines with two network interfaces.
- Confirm that peer-hub-to-spoke is the only peering in the preview.
What-If Preview Failed?
- Check that PowerShell is still inside the project-03-vnet-peering folder.
- Check that the temporary password satisfies the required length and complexity.
- Confirm that main.bicep still compiles without diagnostics.
Need another set of eyes? Help me diagnose this Bicep what-if failure.
Deploy the incomplete topology
This is the point where paid Azure resources start running. The resource group keeps the complete lab inside one cleanup boundary.
Azure can take several minutes to provision the network and compute resources. A quiet terminal during part of that wait does not mean the deployment has stalled.
- Start the initial deployment by running the command below.
- Enter the same temporary password when Azure CLI prompts for adminPassword.
az deployment group create `
--name project03-initial `
--resource-group $ResourceGroup `
--template-file .\main.bicep `
--parameters completePeering=false restrictIcmpToVm=false
What Does This Deployment Create?
- The deployment name project03-initial identifies this first version of the topology.
- The template creates separate hub and spoke address spaces with private workload subnets.
- Each subnet receives an NSG and an explicit outbound path through its own NAT Gateway.
- The two VM network interfaces use static private addresses without VM public IPs.
- Only peer-hub-to-spoke is deployed because reciprocal peering remains disabled.
When the command finishes, PowerShell returns the deployment result. The temporary password remains absent from every repository file.
Deployment Did Not Complete?
- Check the deployment output for the first resource that failed.
- Retry with the same temporary password if the prompt was interrupted.
- Confirm that the subscription still has capacity for two Standard_B1s VMs in eastus.
If the cause is unclear, help me interpret the failed Azure deployment output.
Paid Resources Are Running
NAT Gateway billing starts when each gateway is created. A partial hour is billed as a full hour.
The VMs incur compute charges while allocated. Managed disks and static public IPs can continue to incur charges.
Peering traffic and NAT data processing can also incur charges.
- Continue directly through testing and cleanup to keep this paid lab short-lived.
Verify the deployment and resources
The deployment state proves that Azure accepted the complete template operation. The resource inventory then confirms that the expected network and compute components exist.
Before you run this, make a quick prediction about the deployment state you expect to see.
- Query the initial deployment state by running this command:
az deployment group show `
--resource-group $ResourceGroup `
--name project03-initial `
--query properties.provisioningState `
--output tsv
What Does This Check Prove?
- The command reads the deployment named project03-initial from the Project 03 resource group.
- The query selects only the provisioning state.
- The tab-separated output format prints the state as one plain value.
You should see Succeeded. That result confirms the initial deployment completed.
- List the deployed resources by running this command:
az resource list --resource-group $ResourceGroup --output table
What Should I See?
The table inventories the Azure resources inside rg-az104-project03.
- Two VMs named vm-project03-hub and vm-project03-spoke.
- Two network interfaces named nic-project03-hub and nic-project03-spoke.
- Two VNets named vnet-project03-hub and vnet-project03-spoke.
- Two NSGs named nsg-project03-hub and nsg-project03-spoke.
- Two NAT gateways with their two Standard static public IP resources.
- Two managed OS disks for the private VMs.
That is the first live milestone complete. The private hub and spoke resources now match the preview.
The successful deployment also confirms that peer-hub-to-spoke exists while peer-spoke-to-hub remains absent.
Missing an Expected Resource?
- Confirm that the deployment state above printed Succeeded.
- Check that the resource table belongs to rg-az104-project03.
- Scroll through the full table if the PowerShell window hides some rows.
Still missing something? Help me compare my resource inventory with the expected Project 03 topology.
Your private network lab is live with only the hub-to-spoke peering deployed. Next, you will inspect that one-sided peering and test the private data path.
Prove the Peering Is Incomplete
Your initial Microsoft Azure topology is live with one direction of VNet peering. This step gathers evidence of what that one-sided connection means for the private path.
You will separate the Azure VM Run Command management path from the private data path. You will compare the peering state with a private ping before documenting the diagnosis.
In this step, get ready to:
- Inspect the existing peering state.
- Test the hub-to-spoke private path through Run Command.
- Document the private-path result in README.md.
Inspect the peering state
The peering state provides direct control-plane evidence about the existing link. The query targets only peer-hub-to-spoke under the hub VNet.
Before you run this, what state do you expect a one-sided peering to report?
- Query peer-hub-to-spoke by running this command:
az network vnet peering show `
--resource-group $ResourceGroup `
--vnet-name vnet-project03-hub `
--name peer-hub-to-spoke `
--query peeringState `
--output tsv
What does this command check?
- The command reads the existing peer-hub-to-spoke resource under vnet-project03-hub.
- The --query peeringState argument limits the result to the connection state.
- The --output tsv argument prints the state without a table wrapper.
You will see Initiated. This confirms that Azure knows about the first link while peer-spoke-to-hub remains absent.
- Record project03-initial plus peer-hub-to-spoke peeringState=Initiated in the evidence section of README.md.
- Save README.md.
Good work. You now have direct control-plane evidence that the first peering link remains incomplete.
Peering state missing?
- Confirm that PowerShell remains signed in to the intended Azure subscription.
- Check that the resource group is spelled rg-az104-project03.
- Check that the VNet and peering names match vnet-project03-hub and peer-hub-to-spoke.
Still missing the state? Help me diagnose this Azure VNet peering query.
Test the private path
Run Command uses the VM agent management channel to execute a shell command on the private hub VM. The embedded ping tests the separate private path to the spoke VM.
Before you run this, do you expect management access to guarantee that the private ping also succeeds?
- Test the private path from vm-project03-hub by running this command:
az vm run-command invoke `
--resource-group $ResourceGroup `
--name vm-project03-hub `
--command-id RunShellScript `
--scripts "ping -c 4 10.31.1.4 || true" `
--query "value[0].message" `
--output tsv
What does this test prove?
- Run Command asks the VM agent on vm-project03-hub to execute the shell test without opening SSH.
- The existing NAT Gateway provides the outbound path used to return the management result.
- The ping -c 4 10.31.1.4 command sends four ICMP requests to the spoke VM's private address.
- The || true suffix allows Run Command to return the packet-loss output after an unsuccessful ping.
You will see packet-loss output instead of replies from 10.31.1.4. This is the intended shortfall in the one-sided topology.
- Record the failed Run Command ping from vm-project03-hub to 10.31.1.4 in README.md.
- Save README.md.
That is the designed obstacle captured. Run Command reached the VM while the private data-plane test produced packet loss.
Run Command result unexpected?
- If no command message returns, confirm that the target VM is vm-project03-hub.
- If the ping returns replies, confirm that peer-spoke-to-hub has not been deployed.
- If the command remains pending, allow the VM agent time to return its result.
Need help interpreting the result? Help me separate the Run Command management path from the private ping data path.
Record the diagnosis
The evidence now separates two paths. Run Command reached the hub VM through its management channel while the VM-to-VM private path failed.
Your evidence only needs resource states plus command output. The temporary VM password stays out of README.md.
Build the diagnosis
- Management path: Azure VM Run Command reached the hub VM.
- Data path: The hub VM could not reach the spoke VM's private address.
- Root cause: Only the first peering link exists, so the peering remains Initiated.
- Add a diagnosis section to README.md using the three points above.
- Save README.md.
README checkpoint
- The deployment evidence identifies project03-initial.
- The peering evidence records peer-hub-to-spoke peeringState=Initiated.
- The ping evidence records packet loss from vm-project03-hub to 10.31.1.4.
- The diagnosis distinguishes the working management path from the failed data path.
- The evidence contains no temporary VM password.
Evidence looks incomplete?
Compare your evidence section with the checkpoint above. Keep the deployment name, peering state, ping result, and diagnosis in separate entries.
Need help tightening the diagnosis? Help me review my Project 03 failure evidence.
Before the final check, do you expect either result to change after editing only README.md?
- Recheck the incomplete topology by running these commands:
az network vnet peering show `
--resource-group $ResourceGroup `
--vnet-name vnet-project03-hub `
--name peer-hub-to-spoke `
--query peeringState `
--output tsv
az vm run-command invoke `
--resource-group $ResourceGroup `
--name vm-project03-hub `
--command-id RunShellScript `
--scripts "ping -c 4 10.31.1.4 || true" `
--query "value[0].message" `
--output tsv
What does the final check compare?
- The first command reads the current control-plane state of peer-hub-to-spoke.
- The second command reruns the private data-plane test from the hub VM.
The first result prints Initiated. The ping result reports packet loss instead of replies from 10.31.1.4.
Final results differ?
- If the first result differs from Initiated, confirm that the reciprocal peering remains absent.
- If the ping returns replies, confirm that the target address is 10.31.1.4.
- If Run Command returns no result, confirm that the hub VM remains running.
Still seeing different results? Help me verify the designed one-sided peering state.
You now have reproducible evidence that a deployed peering object does not provide a working two-way private path by itself.
Your repository now captures the incomplete peering state plus its data-plane impact. Next, you will add the reciprocal peering and repeat the same tests to prove the repair.
Complete and Test the Peering
Your earlier test proved that Azure VM Run Command could reach the hub VM through the management path. The private ICMP path still failed while the one-sided peering remained Initiated.
Now you will use Bicep to add the reciprocal VNet peering. The same private traffic test becomes proof that the repaired path works in both directions.
In this step, get ready to:
- Preview the reciprocal peering addition without changing the deployed network.
- Reach Connected on both peering resources.
- Prove bidirectional private connectivity between the two VMs.
Preview the reciprocal change
A Bicep what-if compares the complete topology with the live resources. Provider-level validation asks Azure to evaluate the proposed resource change before applying it.
A password prompt here is expected because adminPassword is a secure parameter. Using the same temporary password only at the prompt keeps it out of your repository.
- Return to the PowerShell session in project-03-vnet-peering from earlier.
- Preview the complete topology by running:
az deployment group what-if `
--resource-group $ResourceGroup `
--template-file .\main.bicep `
--parameters completePeering=true restrictIcmpToVm=false `
--validation-level Provider
What does this preview do?
- The what-if operation compares the requested topology with the resources already deployed.
- The completePeering=true parameter activates the conditional peer-spoke-to-hub resource.
- The restrictIcmpToVm=false parameter preserves the current remote VNet CIDR sources in both NSGs.
- The --validation-level Provider option includes provider-level checks in the preview.
- Review the resource-level changes for peer-spoke-to-hub.
You should see peer-spoke-to-hub as the meaningful addition. Secure values or service-managed defaults may create extra preview noise.
Preview does not show the reciprocal peering?
- Confirm that PowerShell is still inside the project-03-vnet-peering folder.
- Check that the parameters read completePeering=true and restrictIcmpToVm=false.
- Verify that $ResourceGroup still contains rg-az104-project03.
Still seeing an unexpected preview? Help me diagnose why the reciprocal peering is missing from my Bicep what-if output.
Deploy and verify both peering states
The template already defines spokeToHubPeering as a conditional resource. Setting completePeering to true creates that missing relationship.
This deployment requests the same temporary password from the initial deployment. Expect Azure to take a few minutes while it checks the existing resources.
- Deploy the reciprocal peering by running:
az deployment group create `
--name project03-complete `
--resource-group $ResourceGroup `
--template-file .\main.bicep `
--parameters completePeering=true restrictIcmpToVm=false
What does this deployment change?
- The deployment is recorded under the name project03-complete.
- The completePeering=true parameter creates peer-spoke-to-hub under vnet-project03-spoke.
- The restrictIcmpToVm=false parameter keeps the hub NSG source at 10.31.0.0/16.
- The same parameter keeps the spoke NSG source at 10.30.0.0/16.
When the command finishes, the deployment output should show Succeeded for project03-complete.
Deployment did not succeed?
- Confirm that you entered the same temporary VM password used for the initial deployment.
- Check the deployment output for the first resource that Azure could not update.
- Confirm that the active subscription still contains rg-az104-project03.
Need help reading the deployment failure? Help me troubleshoot the failed project03-complete deployment.
Before you query both sides, do you expect only the new peering to connect or both peering resources to update?
- Query both peering states by running:
az network vnet peering show `
--resource-group $ResourceGroup `
--vnet-name vnet-project03-hub `
--name peer-hub-to-spoke `
--query peeringState `
--output tsv
az network vnet peering show `
--resource-group $ResourceGroup `
--vnet-name vnet-project03-spoke `
--name peer-spoke-to-hub `
--query peeringState `
--output tsv
Why do both states change?
- The first command checks the original peer-hub-to-spoke relationship.
- The second command checks the new peer-spoke-to-hub relationship.
- Azure marks both relationships as Connected once each VNet points to the other.
There it is. Both commands now print Connected, which proves Azure recognizes the reciprocal relationship.
Still seeing Initiated?
- Confirm that project03-complete finished successfully before querying the peerings.
- Check that the second query uses vnet-project03-spoke with peer-spoke-to-hub.
- Verify that both commands target rg-az104-project03.
Still missing a Connected state? Help me trace why one Azure VNet peering remains Initiated.
- Add project03-complete plus both Connected results to the evidence section in README.md.
- Save README.md.
Prove the data path in both directions
Connected peering states confirm the control-plane configuration. The private data path still needs a traffic test from each VM.
The existing Network Security Groups still allow ICMP from the remote VNet CIDRs. Reciprocal peering is the changed network condition in this test.
Before you run these checks, do you expect the private addresses that failed earlier to return all four replies now?
- Run the bidirectional private ping tests by running:
az vm run-command invoke `
--resource-group $ResourceGroup `
--name vm-project03-hub `
--command-id RunShellScript `
--scripts "ping -c 4 10.31.1.4" `
--query "value[0].message" `
--output tsv
az vm run-command invoke `
--resource-group $ResourceGroup `
--name vm-project03-spoke `
--command-id RunShellScript `
--scripts "ping -c 4 10.30.1.4" `
--query "value[0].message" `
--output tsv
What do these tests prove?
- The first command runs four ICMP requests from vm-project03-hub to 10.31.1.4.
- The second command runs four ICMP requests from vm-project03-spoke to 10.30.1.4.
- The output query isolates the guest command result returned through Azure VM Run Command.
Each result should report four received packets with 0% packet loss.
That is the repair proven. Both private VMs can now exchange ICMP traffic across the reciprocal peering.
One direction still loses packets?
- Confirm that both peering queries print Connected.
- Check that the hub test targets 10.31.1.4.
- Check that the spoke test targets 10.30.1.4.
- Verify that the NSG source CIDRs remain 10.31.0.0/16 for the hub and 10.30.0.0/16 for the spoke.
Still seeing packet loss? Help me troubleshoot one-way ICMP failure across my Connected Azure VNet peerings.
- Add both successful Run Command outputs to the evidence section in README.md with their traffic directions clearly labelled.
- Save README.md.
- Confirm that the evidence section records project03-complete, both Connected states, both four-packet results, and 0% packet loss without including the temporary password.
Your reciprocal peering now carries private traffic in both directions. Next, you will turn the failure and recovery sequence into reusable Project 03 evidence.
Document the Evidence and Handoff
Your Microsoft Azure networking lab now has reciprocal VNet peering. Both private VMs complete four-packet pings with 0% packet loss.
A reproducible handoff turns that working lab into evidence that survives beyond this session. This step prepares the GitHub repository for Project 04.
In this step, get ready to:
- Finish the Project 03 README as portfolio-ready evidence.
- Update the continuation brief with the Project 03 record.
- Publish the completed evidence to GitHub.
Finish the Project 03 README
A portfolio-ready README lets another person follow the failure from first symptom to verified repair. It also connects every claim to a visible result.
What Architecture Facts Should You Capture?
- The hub VNet uses 10.30.0.0/16 with workload subnet 10.30.1.0/24.
- The spoke VNet uses 10.31.0.0/16 with workload subnet 10.31.1.0/24.
- Each workload subnet uses its own Network Security Group.
- Each workload subnet sends explicit outbound traffic through its own NAT Gateway.
- The private Standard_B1s Ubuntu VMs use 10.30.1.4 and 10.31.1.4.
- The reciprocal links are peer-hub-to-spoke and peer-spoke-to-hub.
- Switch back to project-03-vnet-peering/README.md in Visual Studio Code.
- Write an architecture section that explains the two private VNets.
- Place a text diagram under the architecture section that traces the private path between both VMs.
- Preview README.md in the editor.
You should see the architecture as rendered Markdown. The diagram should make the reciprocal path visible without requiring the Azure portal.
- Add relative links to main.bicep and cleanup.ps1.
- Add an evidence timeline that begins with the Initiated peering state.
- Include the failed hub-to-spoke ping in that timeline.
- Explain that Azure VM Run Command proved the management path was working.
- Explain that the failed private ping proved the data path was blocked.
- Refresh the Markdown preview.
The rendered timeline should now separate the working management channel from the failed private data path. That distinction makes the diagnosis defensible.
- Extend the timeline with the reciprocal peering repair.
- Record both Connected peering results.
- Record both successful four-packet pings with 0% packet loss.
- Refresh the Markdown preview.
You should see a complete failure-to-recovery story. The same private addresses that failed now prove the repaired path in both directions.
- Add an operations note covering the active Azure costs.
- Include .\cleanup.ps1 as the cleanup command.
- Search the entire README for the temporary VM password.
- Save README.md.
- Preview the completed README one final time.
The finished README should show the architecture, failure, diagnosis, repair, cost warning, and cleanup path. No password should appear anywhere in the rendered evidence.
Update the continuation brief
The continuation brief gives the next project a reliable starting point. Its Project 03 entry must preserve the evidence that justifies the next recommendation.
What Belongs in the Project 03 Entry?
- The identity details include the project title, completion date, repository path, and resource group.
- The network evidence includes the Initiated failure and both Connected results.
- The traffic evidence includes the failed ping and both successful private pings.
- The learning record covers skills gained and cost notes.
- The Resource state value is Active until the Before You Go choice is completed.
- Select docs/NEXT-PROJECT-BRIEF.md in the Explorer sidebar.
- Remove the second duplicate Completed projects heading.
- Add the Project 03 entry using the checklist above.
- Preview docs/NEXT-PROJECT-BRIEF.md in the editor.
You should see one Completed projects heading. The Project 03 entry should identify project-03-vnet-peering and rg-az104-project03.
Reusable Handoff Checklist
- The finished current project is copied into Completed projects.
- Each completed project records its date, repository path, visible evidence, skills, costs, and final resource state.
- Current direction advances to the next project number.
- The next recommendation explains how it follows from the latest evidence.
- The confirmed tool inventory stays current.
- Every repository link resolves without exposing a secret.
- Change Current direction to Project 04.
- Recommend secure Azure Storage with private access as the next progression.
- Add the reusable completed-project handoff process from the checklist above.
- Refresh the Markdown preview.
The brief should now point Project 04 toward secure Azure Storage with private access. The reason should follow from the private networking evidence you completed here.
- Update the confirmed tool inventory with Microsoft Azure, Azure CLI, Bicep, PowerShell, Visual Studio Code, Git, and GitHub.
- Save docs/NEXT-PROJECT-BRIEF.md.
- Check every repository link in the rendered brief.
- Search the brief for passwords or other secrets.
- Preview the completed brief one final time.
The handoff now preserves the evidence, next direction, tool inventory, and secret checks. That gives Project 04 a usable starting point without relying on memory.
Commit and verify the handoff
A local documentation update becomes durable once Git records it and GitHub receives it. The final status check proves that every intended change reached the commit.
Protect the Temporary Password
A push copies committed content to the remote repository. Your README and continuation brief should contain evidence only, with no temporary VM password.
- Return to the PowerShell session in project-03-vnet-peering.
Before you run these commands, what do you expect the final status check to report after the push?
- Publish the completed documentation by running these commands:
git status
git add .
git commit -m "Complete Azure networking project 03"
git push origin HEAD
git status
What Do These Commands Prove?
- The first git status shows the documentation changes waiting to be recorded.
- The staging command prepares the repository changes for one commit.
- The commit records the Project 03 evidence with the required message.
- The push sends the current commit to the connected remote repository.
- The final git status verifies that the working tree is clean.
The commit and push should complete successfully. The final status should report a clean working tree.
Push Did Not Finish?
- If the commit has no saved changes, return to both Markdown files and save them.
- If the push cannot authenticate, restore the GitHub connection used by the repository.
- If the final status still lists files, save them before repeating the publish sequence above.
Still stuck? Help me diagnose why my Project 03 documentation did not commit or push successfully.
- Go to the connected GitHub repository in your browser.
- Refresh the repository page.
- Select the project-03-vnet-peering folder.
- Confirm that README.md, main.bicep, and cleanup.ps1 are visible.
- Return to the repository root.
- Select the docs folder.
- Select NEXT-PROJECT-BRIEF.md.
- Confirm that the Project 03 entry and Project 04 direction are visible.
That is the handoff secured. Your remote repository now carries the full Project 03 evidence plus a reusable path into Project 04.
Secret mission
Restrict ICMP to the Exact Remote VM
Your repaired network allows ICMP from each remote VNet CIDR. Narrow both security rules to the exact remote VM addresses. Then prove the bidirectional private path still works.
Clean Up Your Resources
Clean Up Your Resources
Your paid Microsoft Azure topology remains active in rg-az104-project03. Several resources continue to incur charges while this resource group exists.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Billing for each NAT Gateway began when Azure created it. Azure bills a partial hour as a full hour.
The virtual machines, managed disks, static public IPs, peering traffic, and NAT data processing can also incur charges. Delete the resource group when you finish using the lab.
Resources you used:
- The Azure resource group rg-az104-project03 in eastus.
- Two Azure virtual networks named vnet-project03-hub and vnet-project03-spoke. Each VNet contains a child subnet named snet-workload.
- Two Network Security Groups named nsg-project03-hub and nsg-project03-spoke. Their ICMP rules allow only the exact remote VM address.
- Two NAT gateways named nat-project03-hub and nat-project03-spoke. Their static public IP resources are pip-project03-hub-nat and pip-project03-spoke-nat.
- Two private Azure virtual machines named vm-project03-hub and vm-project03-spoke. Their supporting resources include two NICs and two Standard HDD managed OS disks.
- Two reciprocal VNet peering resources named peer-hub-to-spoke and peer-spoke-to-hub. Both peerings are currently Connected.
Keep everything running
No action is needed. Choose this option only if you plan to continue working with the restricted topology immediately.
- Keep rg-az104-project03 active in Azure.
- Continue using the exact-address ICMP rules for further private connectivity tests.
- Monitor your Azure charges while the VMs, NAT gateways, public IPs, managed disks, and peering traffic remain active.
Pause - I'll come back to this later
Deallocate both VMs to stop their compute allocation. Your networking resources and managed disks remain deployed for later use.
- Switch back to the PowerShell terminal from the lab.
- Deallocate both private VMs by running these commands:
az vm deallocate --resource-group $ResourceGroup --name vm-project03-hub
az vm deallocate --resource-group $ResourceGroup --name vm-project03-spoke
What does deallocation change?
Deallocation stops compute allocation for both VMs. This stops their compute charges.
The NAT gateways, static public IPs, NICs, peerings, NSGs, and managed disks remain deployed. Those resources can continue to incur charges.
Both commands should finish without an Azure CLI error. The remaining topology stays available for your return.
Delete - I don't want to use this again
Deleting the resource group is irreversible. Your committed Bicep template remains safe in GitHub.
- Switch back to the PowerShell terminal in project-03-vnet-peering.
- Delete the complete resource group by running the cleanup script:
.\cleanup.ps1
What does the cleanup script do?
- The script checks whether rg-az104-project03 exists.
- Azure deletes the resource group with its child and dependent resources.
- The script waits for the deletion to finish.
- The final check prints false when cleanup is complete.
You should see false on the final line. That result proves the paid Azure topology no longer exists.
Cleanup does not print false?
- Confirm PowerShell is still signed in to the intended Azure subscription.
- Confirm the terminal is inside project-03-vnet-peering.
- Run the cleanup script again after correcting the subscription or folder.
If the script still cannot verify deletion, help me diagnose the cleanup result.
The continuation brief should now reflect the verified cloud state. Record the deletion before making the final repository update.
- Switch back to Visual Studio Code from Step 5.
- Locate the Project 03 Resource state field in docs/NEXT-PROJECT-BRIEF.md.
- Change the field to Deleted and verified with az group exists = false.
- Save docs/NEXT-PROJECT-BRIEF.md.
The local update now needs to reach your remote repository. Git records the verified cleanup as the final Project 03 state.
- Commit the cleanup record by running these commands:
git add .
git commit -m "Verify Project 03 cleanup"
git push origin HEAD
git status
What do these Git commands do?
- The first command stages the continuation brief update.
- The second command records the verified cleanup in a commit.
- The third command sends the commit to the existing remote repository.
- The final command checks the state of your local working tree.
The final status check should report a clean working tree. Your remote repository should contain the cleanup verification commit.
Nice Work!
Nice Work!
You did it! You built a private Microsoft Azure networking lab with Bicep.
You've learned how to:
- Deploy two non-overlapping Azure VNets with private Ubuntu VMs. Give each VM explicit outbound access through its own NAT Gateway. Enforce subnet-level inbound rules with Network Security Groups.
- Use Azure VM Run Command to prove the private path fails while one-sided VNet peering remains Initiated. Add reciprocal peering to reach Connected. Prove bidirectional connectivity with four-packet pings that report 0% packet loss.
- Capture the failure plus repair in README.md. Build cleanup.ps1 to remove rg-az104-project03 through one verified process. Publish a reusable Project 04 handoff in docs/NEXT-PROJECT-BRIEF.md through GitHub.
- Secret Mission: Narrow each ICMP rule from the remote VNet CIDR to the exact remote VM address. Keep both private ping tests at 0% packet loss.
Ready to quiz yourself?