Part 3: Control Traffic with Azure NSGs

Secure a two-tier Azure network with NSG rules and IP flow verification.

Introduction

30 Second Summary

One locked front door does not control who can enter every room inside. Sensitive spaces need their own access rules.

In this project, you will restore a private two-tier lab in Microsoft Azure through an ARM template. You will use Azure Network Security Groups to create a least-privilege web-to-application path whose matching rule is exposed by Azure Network Watcher's IP flow verify.

What You'll Build

Your finished Azure portal view shows vm-1 allowed through Allow-VM1-To-App-HTTP while 10.20.1.5 stays blocked through Deny-Web-To-App-HTTP.

By the end of this project, you'll have:

  • A one-template lab restore that brings vm-1 online at 10.20.1.4. It also brings vm-2 online at 10.20.2.4 with no public IP address.
  • Rule-level evidence that ties the initial Access allowed result to AllowVNetInBound. A later check ties Access denied to Deny-Web-To-App-HTTP.
  • A precise exception that lets only vm-1 reach the application tier on TCP port 80. A test with 10.20.1.5 stays blocked.
  • Secret Mission: Add source-side filtering with nsg-web to prove that an outbound deny can stop a flow permitted by the destination-side policy.

Are there any prerequisites?

You need an active Azure subscription with resource creation permissions. This project assumes you completed or reviewed the first two parts using a Windows device with Microsoft Edge.

Before We Start

Your deleted Part 2 lab gives you a clean starting point. This checkpoint commits you to restoring the minimum private lab in Central US before proving which web-to-application traffic the security policy allows or denies.

Restore the Private Lab

The Part 2 resource group is gone. Your traffic-filtering lab now has no private endpoints to test.

An Azure Resource Manager template recreates the minimum topology in one deployment. This avoids repeating the earlier manual build.

The deployment restores an Azure virtual network with two private Azure Virtual Machines. A shared network security group begins on their network interfaces.

In this step, get ready to:
  • Open the custom ARM template editor.
  • Deploy the restored private lab.
  • Verify the restored network and virtual machines.
Open the custom template editor

A custom deployment lets the Azure portal validate your infrastructure definition before creating resources. You can restore the whole lab from one editor.

  • Return to the Azure portal where you are already signed in.
  • Select Create a resource from the portal home page.
  • Enter template in the marketplace search field.
  • Select Template deployment from the results.
  • Select Create.
  • Choose Build your own template in the editor.

You should see the custom template editor containing sample template content.

Can't reach the template editor?

Check that you selected Template deployment from the marketplace results. Confirm that your Azure subscription allows resource creation.

Ask for help with the portal path

Deploy the restored lab

The azuredeploy.json template defines the complete private topology. Keeping the resource definitions together lets Azure create them in dependency order.

  • Replace the sample editor content with the exact azuredeploy.json template shown in the double-check tab below.

✔️ Awesome, I've got everything!

Use this path if you already have the complete azuredeploy.json template ready to paste.

ⓧ I'd like to double check the full code

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "metadata": {
        "description": "Location for all lab resources."
      }
    },
    "adminUsername": {
      "type": "string",
      "defaultValue": "azureuser",
      "metadata": {
        "description": "Administrator username for both private Linux VMs."
      }
    },
    "adminPassword": {
      "type": "securestring",
      "minLength": 12,
      "metadata": {
        "description": "Administrator password for both private Linux VMs."
      }
    }
  },
  "variables": {
    "vnetName": "vnet-nextwork-lab",
    "webSubnetName": "snet-web",
    "appSubnetName": "snet-app",
    "sharedNsgName": "nsg-lab",
    "vmSize": "Standard_B2s",
    "imageReference": {
      "publisher": "Canonical",
      "offer": "0001-com-ubuntu-server-jammy",
      "sku": "22_04-lts-gen2",
      "version": "latest"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Network/networkSecurityGroups",
      "apiVersion": "2023-09-01",
      "name": "[variables('sharedNsgName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": []
      }
    },
    {
      "type": "Microsoft.Network/virtualNetworks",
      "apiVersion": "2023-09-01",
      "name": "[variables('vnetName')]",
      "location": "[parameters('location')]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.20.0.0/16"
          ]
        },
        "subnets": [
          {
            "name": "[variables('webSubnetName')]",
            "properties": {
              "addressPrefix": "10.20.1.0/24"
            }
          },
          {
            "name": "[variables('appSubnetName')]",
            "properties": {
              "addressPrefix": "10.20.2.0/24"
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Network/networkInterfaces",
      "apiVersion": "2023-09-01",
      "name": "vm-1-nic",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkSecurityGroups', variables('sharedNsgName'))]",
        "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]"
      ],
      "properties": {
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('sharedNsgName'))]"
        },
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "primary": true,
              "privateIPAddress": "10.20.1.4",
              "privateIPAddressVersion": "IPv4",
              "privateIPAllocationMethod": "Static",
              "subnet": {
                "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('webSubnetName'))]"
              }
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Network/networkInterfaces",
      "apiVersion": "2023-09-01",
      "name": "vm-2-nic",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkSecurityGroups', variables('sharedNsgName'))]",
        "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]"
      ],
      "properties": {
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('sharedNsgName'))]"
        },
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "primary": true,
              "privateIPAddress": "10.20.2.4",
              "privateIPAddressVersion": "IPv4",
              "privateIPAllocationMethod": "Static",
              "subnet": {
                "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('appSubnetName'))]"
              }
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Compute/virtualMachines",
      "apiVersion": "2023-09-01",
      "name": "vm-1",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkInterfaces', 'vm-1-nic')]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "[variables('vmSize')]"
        },
        "storageProfile": {
          "imageReference": "[variables('imageReference')]",
          "osDisk": {
            "createOption": "FromImage",
            "deleteOption": "Delete",
            "managedDisk": {
              "storageAccountType": "Standard_LRS"
            }
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', 'vm-1-nic')]",
              "properties": {
                "primary": true
              }
            }
          ]
        },
        "osProfile": {
          "computerName": "vm-1",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]",
          "linuxConfiguration": {
            "disablePasswordAuthentication": false,
            "provisionVMAgent": true
          }
        },
        "diagnosticsProfile": {
          "bootDiagnostics": {
            "enabled": false
          }
        }
      }
    },
    {
      "type": "Microsoft.Compute/virtualMachines",
      "apiVersion": "2023-09-01",
      "name": "vm-2",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkInterfaces', 'vm-2-nic')]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "[variables('vmSize')]"
        },
        "storageProfile": {
          "imageReference": "[variables('imageReference')]",
          "osDisk": {
            "createOption": "FromImage",
            "deleteOption": "Delete",
            "managedDisk": {
              "storageAccountType": "Standard_LRS"
            }
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', 'vm-2-nic')]",
              "properties": {
                "primary": true
              }
            }
          ]
        },
        "osProfile": {
          "computerName": "vm-2",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]",
          "linuxConfiguration": {
            "disablePasswordAuthentication": false,
            "provisionVMAgent": true
          }
        },
        "diagnosticsProfile": {
          "bootDiagnostics": {
            "enabled": false
          }
        }
      }
    }
  ],
  "outputs": {
    "vm1PrivateIp": {
      "type": "string",
      "value": "10.20.1.4"
    },
    "vm2PrivateIp": {
      "type": "string",
      "value": "10.20.2.4"
    }
  }
}

What Does This Template Restore?

  • The parameters collect the deployment location and administrator username. The secure password parameter keeps the password out of the template file.
  • The network resources create vnet-nextwork-lab with address space 10.20.0.0/16.
  • The two subnets use 10.20.1.0/24 for snet-web and 10.20.2.0/24 for snet-app.
  • The network interfaces give vm-1 the private address 10.20.1.4. They give vm-2 the private address 10.20.2.4.
  • The empty securityRules collection leaves nsg-lab with default rules only.
  • Select Save in the template editor.

You should see a custom deployment form with fields for the template parameters.

Know What Starts Billing

This deployment starts two billed Standard_B2s virtual machines. Their combined Central US compute cost is about $0.10 per hour.

Managed disks also incur charges. The cleanup section shows you how to stop or delete the resources when you finish.

  • Choose your active Azure subscription in the Subscription field.
  • Use the Resource group control to create NextWork-Networking.

The deployment scope should now show your subscription with NextWork-Networking as the target resource group.

  • Set the resource group region to Central US.
  • Confirm that the Location parameter shows Central US.

The resource group and every template resource should now target Central US.

  • Keep azureuser in the Admin Username field.
  • Enter your secure password in the Admin Password field.

Your password must contain at least 12 characters. The portal masks it while the username remains visible.

  • Select Review + create.

Azure should report that validation passed. This confirms that the template and required values are ready for deployment.

Provisioning can take several minutes while Azure creates the network interfaces and starts both virtual machines. A quiet progress screen during this wait is normal.

  • Select Create to submit the validated deployment.

You should see the deployment progress page. Wait until Azure reports that the deployment completed.

Deployment Validation Failed?

Return to the template editor if Azure highlights a template problem. Confirm that you replaced all sample content with the complete azuredeploy.json file.

Use a different administrator password if the password field is rejected. Keep the password private.

Ask for help with the ARM deployment

Verify the private topology

A completed deployment confirms that Azure accepted the template. The resource views confirm that the resulting topology matches the lab design.

  • Return to NextWork-Networking from the deployment summary.
  • Select vnet-nextwork-lab from the resource list.
  • Use the Overview page to confirm the address space is 10.20.0.0/16.
  • Select Subnets from the virtual network menu.
  • Confirm that snet-web uses 10.20.1.0/24.
  • Confirm that snet-app uses 10.20.2.0/24.

The virtual network now has the same two-subnet layout as the earlier lab.

  • Return to the NextWork-Networking resource list.
  • Select nsg-lab.
  • Open the network interface associations for nsg-lab.
  • Confirm that vm-1-nic is associated with nsg-lab.
  • Confirm that vm-2-nic is associated with nsg-lab.
  • Open Inbound security rules.
  • Confirm that nsg-lab has no custom security rules.

The shared NSG is attached at the NIC level. Its default rules provide the baseline that you test in the next step.

Azure normally enables Network Watcher when a virtual network is created. Confirming the Central US instance ensures the diagnostic tool is available for the upcoming traffic check.

  • Enter network watcher in the Azure portal search bar.
  • Select Network Watcher from the results.
  • Check whether Network Watcher is enabled in Central US.

✔️ Central US is enabled

Network Watcher is ready for the IP flow checks in the next step.

ⓧ Central US is missing

Your subscription may have automatic Network Watcher enablement turned off. Create the regional instance before continuing.

  • Select + Create.
  • Choose the subscription containing NextWork-Networking.
  • Choose Central US as the region.
  • Select Add.

You should see Central US listed after the regional Network Watcher instance is created.

Before the final check, which private address do you expect each virtual machine to show?

  • Return to the NextWork-Networking resource group.
  • Select Virtual machines.
  • Confirm that vm-1 shows Running.
  • Select vm-1.
  • Use its Overview page to confirm the private IP address is 10.20.1.4.
  • Confirm that no public IP address is assigned to vm-1.

The first VM is running inside snet-web with only its planned private address.

  • Return to the Virtual machines page.
  • Confirm that vm-2 shows Running.
  • Select vm-2.
  • Use its Overview page to confirm the private IP address is 10.20.2.4.
  • Confirm that no public IP address is assigned to vm-2.

That restore is complete. Both private Ubuntu 22.04 LTS Gen2 VMs are running with the exact addresses required for repeatable traffic tests.

VM Missing or Not Running?

Return to the deployment summary and inspect the failed resource if one VM is missing. A password policy or regional capacity issue can prevent a VM from finishing deployment.

Ask for help with the restored VM

Your private two-tier lab is restored. Next, you will test the default path that still permits web-to-app traffic.

Expose the Default Allow Path

Your restored private lab now has a web-tier source at 10.20.1.4. Its application-tier destination is 10.20.2.4.

The nsg-lab Azure Network Security Group is attached to both network interfaces. Its default AllowVNetInBound rule still permits traffic inside the virtual network.

Azure Network Watcher's IP flow verify evaluates packet details against every NSG applied to a VM network interface. It reports the access decision plus the rule responsible for it.

In this step, get ready to:
  • Target vm-2 through vm-2-nic in IP flow verify.
  • Model an inbound TCP packet from the web subnet to the application subnet.
  • Confirm which default rule allows the packet through.
Target the application VM

The diagnostic evaluates traffic from the target VM's perspective. For this baseline, vm-2 is the destination protected by vm-2-nic.

  • Return to the Azure portal from the previous step.
  • Enter network watcher in the portal search bar.
  • Select Network Watcher from the search results.
  • Select IP flow verify under Network diagnostic tools.
  • Choose vm-2 in Target resource.
  • Choose vm-2-nic in Network interface.

The diagnostic now targets the application VM's network interface.

Define the inbound packet

An NSG evaluates a five-tuple for each packet. The tuple describes both endpoints plus the protocol.

  • Set Protocol to TCP.
  • Set Direction to Inbound.
  • Set the local IP to 10.20.2.4.
  • Enter 80 in Local port.
  • Enter 10.20.1.4 in Remote IP address.
  • Enter 60000 in Remote port.

How does this packet map to the lab?

  • The remote endpoint 10.20.1.4:60000 represents the source at vm-1.
  • The local endpoint 10.20.2.4:80 represents the destination at vm-2.
  • The inbound direction evaluates the packet as it reaches vm-2-nic.
  • The protocol completes the packet definition with TCP.
Verify the default allow rule

Before you run the check, pause on which rule you expect to decide this packet.

  • Select Verify IP flow.

You found the open path. The result shows Access allowed and names AllowVNetInBound as the matching security rule.

Why does this flow pass?

An NSG includes default rules even when it has no custom rules. AllowVNetInBound permits traffic whose source and destination match VirtualNetwork.

Both private IP addresses belong to vnet-nextwork-lab. Separate subnet address ranges alone do not filter traffic.

🙋‍♀️ Not seeing the allowed result?

  • Confirm vm-2 is still running because stopped VMs are unavailable to IP flow verify.
  • Check that Network interface is set to vm-2-nic.
  • Compare each packet value with the tuple 10.20.1.4:60000 to 10.20.2.4:80.

Help me diagnose my IP flow verify result.

Your baseline proves that the application tier remains reachable through the default VNet rule. Next up, you'll test whether moving protection to snet-app changes that decision.

Move Protection to the Application Subnet

Your earlier IP flow verify baseline in Network Watcher proved that the current Azure Network Security Groups permit web-to-app traffic. Your next goal is to give the application subnet one clear policy boundary.

Filtering at the subnet level plus the NIC level makes inbound decisions depend on two rule sets. This step moves protection to the application subnet.

In this step, get ready to:
  • Create nsg-app as the policy boundary for snet-app.
  • Remove nsg-lab from both NICs.
  • Rerun the baseline flow check to reveal the unchanged default allow decision.
Create and associate nsg-app

A subnet-level NSG applies its policy to the network interfaces inside that subnet. nsg-app becomes the shared boundary for snet-app.

Why Subnet-Level Filtering?

A shared subnet association keeps the application tier's policy in one place. Every NIC in snet-app sits behind the same boundary.

Dual associations make inbound traffic pass both rule sets. A single layer makes the matching decision easier to trace.

  • Enter Network security group in the Azure portal search bar.
  • Select Network security groups from the results.
  • Select + Create.
  • Choose the subscription that contains NextWork-Networking.
  • Select NextWork-Networking as the resource group.

The form now targets the restored lab in NextWork-Networking.

  • Enter nsg-app as the name.
  • Select Central US as the region.

You should now see nsg-app configured for the same resource group and region as the lab.

  • Select Review + create.

Azure checks the resource details. You should see the configuration pass validation.

Creating this resource may take a moment. A short wait at the deployment screen is expected.

  • Select Create after validation passes.

You will see the deployment finish successfully. The new nsg-app resource is ready for its subnet association.

  • Return to the Azure portal search bar.
  • Enter nsg-app.
  • Select nsg-app from the results.
  • Select Subnets from the resource menu.
  • Select + Associate.
  • Choose vnet-nextwork-lab as the virtual network.

The subnet picker now shows the subnets inside vnet-nextwork-lab.

  • Choose snet-app as the subnet.
  • Select OK.

Good progress. nsg-app now lists snet-app on its Subnets page.

Can't Associate the Subnet?

  • Confirm that nsg-app was created in Central US.
  • Confirm that you selected vnet-nextwork-lab before choosing snet-app.

Help me associate nsg-app with snet-app.

Remove nsg-lab from both NICs

Inbound traffic checks a subnet NSG before a NIC NSG. Both layers must allow a flow when both associations exist.

The nsg-lab resource remains available after dissociation. Removing its NIC associations leaves the application subnet with one policy boundary.

  • Enter vm-1-nic in the Azure portal search bar.
  • Select vm-1-nic from the results.
  • Select Network security group from the vm-1-nic menu.
  • Select None.
  • Select Save.

The vm-1-nic page now shows its network security group as None.

  • Enter vm-2-nic in the Azure portal search bar.
  • Select vm-2-nic from the results.
  • Select Network security group from the vm-2-nic menu.
  • Select None.
  • Select Save.

The vm-2-nic page now shows its network security group as None. The old nsg-lab resource has no NIC associations.

Does a NIC Still Show nsg-lab?

  • Return to that NIC's Network security group page.
  • Select None.
  • Select Save.

Help me remove nsg-lab from the NIC.

Test the subnet boundary

IP flow verify can test the same five-tuple after a policy placement change. Keeping every packet value identical isolates the effect of moving the NSG.

  • Return to Network Watcher from earlier.
  • Select IP flow verify under Network diagnostic tools.
  • Select vm-2 for Target resource.
  • Select vm-2-nic for Network interface.
  • Select TCP for Protocol.
  • Select Inbound for Direction.

Keep the Flow Identical

The target remains vm-2. The source remains vm-1.

Reusing the original ports proves whether the association change altered the same flow.

  • Confirm that the local IP value is 10.20.2.4.
  • Enter 80 for Local port.
  • Enter 10.20.1.4 for Remote IP address.
  • Enter 60000 for Remote port.

Before you verify, do you think changing the NSG's location changes the rule decision by itself?

  • Select Verify IP flow.

You will see Access allowed. The matching rule is AllowVNetInBound under nsg-app.

Why Is the Flow Still Allowed?

This is the intended shortfall. nsg-app has no custom rules.

The default AllowVNetInBound rule still permits traffic inside the virtual network. The application tier now has a clear boundary that needs a targeted rule.

🙋‍♀️ Not Seeing Access Allowed?

  • Confirm that vm-2 remains Running.
  • Compare every IP address and port with the baseline values above.
  • Confirm that nsg-app has no custom security rules.

Help me troubleshoot the IP flow verify result.

Your application subnet now has one clear NSG boundary. Next, you will add the targeted deny rule that turns this open path into an enforced policy.

Block Web-to-App HTTP

The application subnet now has a clear policy boundary through the Azure Network Security Group nsg-app. Your baseline test showed that its default AllowVNetInBound rule still permits the web tier to reach the application tier.

A targeted deny rule will match inbound TCP traffic on HTTP port 80 from snet-web. Priority 200 places it ahead of default priority 65000.

In this step, get ready to:
  • Define the web-subnet traffic targeted by the new inbound rule.
  • Set the rule to deny TCP port 80 at priority 200.
  • Rerun IP flow verify to identify the matching custom rule.
Define the denied traffic

A five-tuple defines the exact traffic an NSG rule matches. This rule starts with the entire 10.20.1.0/24 web subnet as its source.

  • Return to nsg-app in the Azure portal.
  • Select Inbound security rules.
  • Select Add.
  • Set Source to IP Addresses.
  • Enter 10.20.1.0/24 in Source IP addresses/CIDR ranges.
  • Enter * in Source port ranges.
  • Set Destination to Any.
  • Set Service to Custom.
Set the deny decision

The wildcard accepts any source port. Destination port 80 limits the rule to HTTP traffic.

  • Enter 80 in Destination port ranges.
  • Set Protocol to TCP.
  • Set Action to Deny.
  • Enter 200 in Priority.
  • Enter Deny-Web-To-App-HTTP in Name.
  • Select Add to create the rule.

Good work. nsg-app now shows the custom web-to-app deny above its default rules.

Verify the custom denial

Azure Network Watcher evaluates the same packet details against every NSG applied to vm-2-nic. Reusing the baseline tuple proves whether the new rule changed the decision.

  • Return to Network Watcher from earlier in the Azure portal.
  • Select IP flow verify under Network diagnostic tools.
  • Set Target resource to vm-2.
  • Set Network interface to vm-2-nic.

Keep the Tuple Unchanged

A before-and-after test is strongest when the packet details stay identical. The NSG rule is the only changed condition.

  • Set Protocol to TCP.
  • Set Direction to Inbound.
  • Enter 80 in Local port.
  • Enter 10.20.1.4 in Remote IP address.
  • Enter 60000 in Remote port.

Before you verify, ask yourself which rule should match first.

  • Select Verify IP flow.

You'll see Access denied. The result identifies Deny-Web-To-App-HTTP as the matching rule.

Still Seeing Access Allowed?

  • Confirm that Deny-Web-To-App-HTTP appears under Inbound security rules with the Deny action.
  • Check that Source IP addresses/CIDR ranges contains 10.20.1.0/24.
  • Check that Destination port ranges contains 80.

Help me troubleshoot the IP flow result.

The broad web-subnet path is now blocked by a named custom rule. Next, you'll add one precise exception for vm-1.

Add a Higher-Priority Exception

Your broad deny rule in the Azure portal now blocks TCP port 80 from the entire web subnet. That protection also blocks vm-1 from reaching the intended application path.

An Azure Network Security Group evaluates lower priority numbers first. In this step, you'll add a precise priority 100 exception before the existing priority 200 deny rule.

In this step, get ready to:
  • Add a priority 100 rule that allows vm-1 to reach TCP port 80.
  • Verify vm-1 matches the new allow rule with IP flow verify.
  • Compare a second source address against the existing deny rule.
Create the precise allow rule

The new rule matches only the static private address of vm-1. Every other address in snet-web remains covered by the broader deny rule.

  • Return to nsg-app from earlier.
  • Select Inbound security rules in the left menu.
  • Select the control above the rules table for adding an inbound security rule.
  • Select IP Addresses in the Source field.
  • Enter 10.20.1.4 in the Source IP addresses/CIDR ranges field.
  • Enter * in the Source port ranges field.

Why use one private IP?

The source address 10.20.1.4 belongs only to vm-1. This keeps the exception limited to the machine that needs access.

  • Select Any in the Destination field.
  • Select Custom in the Service field.
  • Enter 80 in the Destination port ranges field.
  • Select TCP in the Protocol field.
  • Select Allow in the Action field.
  • Enter 100 in the Priority field.
  • Enter Allow-VM1-To-App-HTTP in the Name field.
  • Submit the new inbound security rule from the form.

Strong work. nsg-app now has a single-host exception without removing its broader protection.

Why does priority 100 win?

Custom NSG priorities run from 100 to 4096. Azure evaluates lower numbers first.

Evaluation stops at the first matching rule. A packet from vm-1 reaches the priority 100 allow rule before the priority 200 deny rule.

Verify the vm-1 exception

The packet details stay the same as your denied baseline. The new priority order should change which custom rule wins.

  • Return to Network Watcher from earlier.
  • Select IP flow verify under Network diagnostic tools.
  • Select vm-2 in the Target resource field.
  • Select vm-2-nic in the Network interface field.
  • Confirm the local IP shows 10.20.2.4.
  • Select TCP in the Protocol field.
  • Select Inbound in the Direction field.

Which end is local?

For an inbound test against vm-2, local describes the destination VM. Remote describes the source of the packet.

This mapping keeps the five-tuple aligned with traffic moving from vm-1 to vm-2.

  • Enter 80 in the Local port field.
  • Enter 10.20.1.4 in the Remote IP address field.
  • Enter 60000 in the Remote port field.

Before you verify the flow, which rule do you think Azure evaluates first for this source address?

  • Select Verify IP flow.

You'll see Access allowed. The matching rule is Allow-VM1-To-App-HTTP.

There it is. Your priority 100 exception now restores the intended path for vm-1.

Still seeing Access denied for vm-1?

  • Confirm Allow-VM1-To-App-HTTP uses priority 100.
  • Confirm its source is 10.20.1.4.
  • Confirm its action is Allow.

Help me troubleshoot the allow rule.

Compare a different web address

Changing only the remote IP isolates the source-matching decision. The broader web-subnet deny rule should handle an address outside the precise exception.

  • Replace 10.20.1.4 with 10.20.1.5 in the Remote IP address field.

Before you verify this comparison, do you expect the single-host allow rule or the subnet-wide deny rule to match?

  • Select Verify IP flow.

You'll see Access denied. The matching rule is Deny-Web-To-App-HTTP.

  • Return to nsg-app from earlier.
  • Select Inbound security rules in the left menu.
  • Confirm Allow-VM1-To-App-HTTP appears at priority 100.
  • Confirm Deny-Web-To-App-HTTP remains at priority 200.

That is least privilege in action. vm-1 gets the path it needs while the rest of snet-web stays blocked.

Secret mission

Block the Flow at Its Source

The destination-side exception now permits vm-1. The source subnet still gets a vote. Add a temporary outbound deny to prove that the flow can be blocked before it leaves snet-web. Remove the rule to restore access.

Clean Up Your Resources

Clean Up Your Resources

Two running Azure Virtual Machines cost about $0.10 per hour in Central US, plus managed disk charges. Decide whether to keep the lab running, pause it for Part 4, or delete it entirely.

Cost warning

Both running VMs continue to incur compute charges. Deallocating them stops VM instance billing.

Managed disks and some networking resources can continue to incur charges after deallocation. Delete the resource group to remove the complete lab.

Resources you used:

  • Azure resource group NextWork-Networking in Central US.
  • The virtual network vnet-nextwork-lab, containing snet-web and snet-app.
  • Private VMs vm-1 and vm-2, including their NICs and managed OS disks.
  • Network security groups nsg-lab, nsg-app, and nsg-web.

Keep everything running

No action needed. Choose this if you are continuing to Part 4 immediately.

  • Keep both VMs running so the private two-subnet lab stays ready for further work.
  • Expect ongoing compute charges of about $0.10 per hour for both running VMs.
  • Retain the rules on nsg-app and the subnet associations for nsg-app and nsg-web.

Pause - I'll come back to this later

Deallocate both VMs to stop their instance billing. This preserves the network and security policy for Part 4.

  • Search for Resource groups in the Azure portal.
  • Select NextWork-Networking from the results.
  • Select vm-1 from the resource list.
  • Select Stop on the Overview page.
  • Wait for vm-1 to finish stopping.

You'll see Stopped (deallocated) on the vm-1 overview.

  • Return to NextWork-Networking.
  • Select vm-2 from the resource list.
  • Select Stop on the Overview page.
  • Wait for vm-2 to finish stopping.

Good call. Both VM instances now show Stopped (deallocated), which stops their compute billing.

Delete - I don't want to use this again

Deleting the resource group is a big step. You can rebuild the lab later by redeploying azuredeploy.json.

This option removes every resource listed above. It also removes the foundation needed for Part 4.

  • Search for Resource groups in the Azure portal.
  • Select NextWork-Networking from the results.
  • Select Delete resource group.
  • Enter NextWork-Networking in the confirmation field.
  • Complete the deletion confirmation.
  • Wait for Azure to finish deleting the resource group.
  • Return to Resource groups.
  • Confirm that NextWork-Networking no longer appears.

That clears the full lab. The VMs, managed disks, virtual network, NICs, and NSGs are removed together.

Nice Work!

Nice Work!

You did it! You completed Part 3 by turning an open web-to-app path into a least-privilege exception in Azure. Your final checks identify the exact rule behind every traffic decision.

You've learned how to:

  • Restore a private two-tier lab from an Azure Resource Manager template. Both private VMs run in separate subnets without public IP resources.
  • Use Azure Network Watcher with IP flow verify to test a five-tuple against each applied network security group. The baseline result names AllowVNetInBound as the default rule permitting TCP port 80.
  • Create a clear subnet-level policy boundary by associating nsg-app with snet-app. Allow-VM1-To-App-HTTP at priority 100 permits vm-1. Deny-Web-To-App-HTTP at priority 200 denies the broader web subnet.
  • Secret Mission: Use source-side outbound filtering on nsg-web to deny a flow that nsg-app otherwise permits. Remove Deny-Web-Egress-To-App-HTTP to restore Access allowed.

Ready to quiz yourself?