Part 4: Azure NAT Gateway Egress Control

Route two private Azure VMs through a shared, static NAT Gateway IP.

Introduction

30 Second Summary

When two computers connect to the internet, the address they show outside can matter just as much as the sites they reach. If that address changes without warning, access rules and troubleshooting become guesswork.

In this project, you will restore a secured two-tier lab from an Azure Resource Manager template. You will give two private Azure Virtual Machines one predictable outbound address with Azure NAT Gateway.

What You'll Build

Your final demo shows both private virtual machines returning the same static public IPv4 address while their subnets remain private.

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

  • A repeatable lab restore you can launch from one template to bring the secured two-tier environment back online.
  • Clear before-and-after proof that reveals each VM's uncontrolled outbound address before showing its controlled address.
  • A subnet-scope demonstration where vm-2 moves onto controlled egress first. vm-1 stays outside the path until you bring both tiers under the same address.
  • Secret Mission: Generate repeated outbound traffic from both VMs. Find the evidence in Azure Monitor's Bytes metric.

Are there any prerequisites?

You'll need an active Azure subscription with permission to manage networking resources and virtual machines. This project assumes you completed or reviewed Part 3.

Before We Start

The Part 3 lab is gone, so this checkpoint locks in the secured network you are about to restore. You will replace its implicit outbound access with a predictable shared path through Azure NAT Gateway.

Restore the Secured Two-Tier Lab

The deleted Part 3 resource group leaves the Azure portal with no lab workloads to test. Controlled outbound access needs the secured two-tier network back first.

An Azure Resource Manager template rebuilds the full topology in one deployment. The same template restores the final Network Security Group boundaries from Part 3.

This repeatable baseline lets you focus on outbound behavior without repeating the earlier manual build.

In this step, get ready to:
  • Confirm access to the intended Azure subscription.
  • Deploy the complete ARM template into Central US.
  • Verify the restored secured topology.
Confirm Azure portal access

The deployment belongs in the subscription you plan to use for this lab. Confirming that subscription now keeps every restored resource together.

  • Press the Windows key to open Windows search.
  • Type Microsoft Edge into the search field.
  • Press Enter to open Microsoft Edge.
  • Search for the Azure portal from the Edge address bar.
  • Select the official Microsoft Azure portal result.

Keep Your Sign-In Private

Azure handles your credentials inside its sign-in flow. Keep your password out of screenshots or notes that other people can access.

  • Select Sign in if the portal asks you to authenticate.
  • Complete sign-in with the Azure account tied to your intended subscription.
  • Use the portal search to find your intended subscription.
  • Select the intended subscription from the search results.

Its status should show as active. You're in, with the intended subscription ready to host the restored lab.

Deploy the ARM template

The template acts as a repeatable blueprint for the lab. Azure reads the resource definitions before creating them in dependency order.

  • Return to the Azure portal search.
  • Search for Deploy a custom template.
  • Select Deploy a custom template from the results.
  • Select Build your own template in the editor.
  • Replace the blank template with the complete azuredeploy.json from the tab that matches your situation:

✔️ I have the complete template

  • Copy the complete contents of azuredeploy.json into the blank template editor.
  • Keep every resource definition unchanged.

ⓧ I'd like to double check the full code

Compare your template with this complete azuredeploy.json file.

Complete Template in Two Parts

Read both parts in order. Part 2 continues directly after the final line of Part 1.

Part 1: Parameters and network resources
{
  "$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."
      }
    },
    "vmSize": {
      "type": "string",
      "defaultValue": "Standard_D2s_v3",
      "metadata": {
        "description": "VM size for both lab machines."
      }
    }
  },
  "variables": {
    "vnetName": "vnet-nextwork-lab",
    "webSubnetName": "snet-web",
    "appSubnetName": "snet-app",
    "legacyNsgName": "nsg-lab",
    "webNsgName": "nsg-web",
    "appNsgName": "nsg-app",
    "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('legacyNsgName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": []
      }
    },
    {
      "type": "Microsoft.Network/networkSecurityGroups",
      "apiVersion": "2023-09-01",
      "name": "[variables('webNsgName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": []
      }
    },
    {
      "type": "Microsoft.Network/networkSecurityGroups",
      "apiVersion": "2023-09-01",
      "name": "[variables('appNsgName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": [
          {
            "name": "Allow-VM1-To-App-HTTP",
            "properties": {
              "priority": 100,
              "protocol": "Tcp",
              "access": "Allow",
              "direction": "Inbound",
              "sourceAddressPrefix": "10.20.1.4",
              "sourcePortRange": "*",
              "destinationAddressPrefix": "*",
              "destinationPortRange": "80"
            }
          },
          {
            "name": "Deny-Web-To-App-HTTP",
            "properties": {
              "priority": 200,
              "protocol": "Tcp",
              "access": "Deny",
              "direction": "Inbound",
              "sourceAddressPrefix": "10.20.1.0/24",
              "sourcePortRange": "*",
              "destinationAddressPrefix": "*",
              "destinationPortRange": "80"
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Network/virtualNetworks",
      "apiVersion": "2023-09-01",
      "name": "[variables('vnetName')]",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkSecurityGroups', variables('webNsgName'))]",
        "[resourceId('Microsoft.Network/networkSecurityGroups', variables('appNsgName'))]"
      ],
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.20.0.0/16"
          ]
        },
        "subnets": [
          {
            "name": "[variables('webSubnetName')]",
            "properties": {
              "addressPrefix": "10.20.1.0/24",
              "networkSecurityGroup": {
                "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('webNsgName'))]"
              }
            }
          },
          {
            "name": "[variables('appSubnetName')]",
            "properties": {
              "addressPrefix": "10.20.2.0/24",
              "networkSecurityGroup": {
                "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('appNsgName'))]"
              }
            }
          }
        ]
      }
    },
Part 2: Interfaces, VMs, and outputs
    {
      "type": "Microsoft.Network/networkInterfaces",
      "apiVersion": "2023-09-01",
      "name": "vm-1-nic",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]"
      ],
      "properties": {
        "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/virtualNetworks', variables('vnetName'))]"
      ],
      "properties": {
        "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": "[parameters('vmSize')]"
        },
        "storageProfile": {
          "imageReference": "[variables('imageReference')]",
          "osDisk": {
            "createOption": "FromImage",
            "managedDisk": {
              "storageAccountType": "Standard_LRS"
            }
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', 'vm-1-nic')]"
            }
          ]
        },
        "osProfile": {
          "computerName": "vm-1",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]",
          "linuxConfiguration": {
            "disablePasswordAuthentication": false,
            "provisionVMAgent": true
          }
        }
      }
    },
    {
      "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": "[parameters('vmSize')]"
        },
        "storageProfile": {
          "imageReference": "[variables('imageReference')]",
          "osDisk": {
            "createOption": "FromImage",
            "managedDisk": {
              "storageAccountType": "Standard_LRS"
            }
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', 'vm-2-nic')]"
            }
          ]
        },
        "osProfile": {
          "computerName": "vm-2",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]",
          "linuxConfiguration": {
            "disablePasswordAuthentication": false,
            "provisionVMAgent": true
          }
        }
      }
    }
  ],
  "outputs": {
    "vm1PrivateIp": {
      "type": "string",
      "value": "10.20.1.4"
    },
    "vm2PrivateIp": {
      "type": "string",
      "value": "10.20.2.4"
    },
    "nextAction": {
      "type": "string",
      "value": "Record each VM's outbound IPv4 address before creating the NAT Gateway."
    }
  }
}

💡 What Does This Template Restore?

  • The parameters object collects the deployment location and VM settings.
  • The variables object keeps the network names and Ubuntu image reference consistent.
  • The template creates nsg-lab, nsg-web, and nsg-app.
  • The nsg-app definition restores the final inbound HTTP boundaries from Part 3.
  • The vnet-nextwork-lab definition creates both workload subnets.
  • The NIC definitions assign static private IPv4 addresses to both VMs.
  • The VM definitions create two private Ubuntu machines without public IP bindings.
  • The outputs object reports both private addresses and the next testing action.
  • Select Save in the template editor.
  • Create the resource group NextWork-Networking in the deployment form.
  • Set the resource group region to Central US.
  • Set location to Central US.
  • Set adminUsername to azureuser.
  • Enter a private password of at least 12 characters for adminPassword.
  • Confirm that vmSize is set to Standard_D2s_v3.
  • Confirm that the size summary shows 1x Standard D2s v3 with 2 vCPUs and 8 GiB of memory.

Charges Start With This Deployment

This deployment creates two Azure Virtual Machines with managed disks. Azure usage charges can begin after resource creation.

The cleanup section shows you how to remove the resource group when you finish.

  • Select Review + create.
  • Confirm that validation succeeds.
  • Select Create.

Azure may spend several minutes provisioning the network resources and VMs. A quiet deployment screen during that time does not mean the process has stopped.

  • Wait for the deployment to finish.

That's the lab rebuilt. One template has restored the network boundaries and both private VMs.

Validation or Deployment Failed?

  • Confirm that both region values use Central US.
  • Confirm that adminPassword contains at least 12 characters.
  • Confirm that vmSize is set to Standard_D2s_v3.
  • Check the quota details if Azure reports OperationNotAllowed. Two D2s v3 VMs require four DSv3-family vCPUs.
  • Select another compatible Gen2 VM size only if your subscription has no available DSv3-family quota.
  • Review the subscription permissions named in the validation details.
  • Share only non-sensitive validation details with the NextWork community.

Ask for help with the deployment details by using help me diagnose my ARM template validation or deployment failure.

Verify the restored topology

A successful deployment confirms that Azure accepted the template. The resource views prove that the deployed state matches the secured Part 3 design.

  • Return to the NextWork-Networking resource group.
  • Select vnet-nextwork-lab.
  • Confirm that the Azure Virtual Network address space is 10.20.0.0/16.
  • Select Subnets.
  • Confirm that snet-web uses 10.20.1.0/24.
  • Confirm that snet-web is associated with nsg-web.
  • Confirm that snet-app uses 10.20.2.0/24.
  • Confirm that snet-app is associated with nsg-app.

The two workload subnets now match the restored two-tier layout.

  • Return to the NextWork-Networking resource group.
  • Confirm that nsg-lab, nsg-web, and nsg-app are listed.
  • Select nsg-app.
  • Open its security rules view.

What Should the App Rules Show?

  • The Allow-VM1-To-App-HTTP rule allows source 10.20.1.4 to reach TCP destination port 80 at priority 100.
  • The Deny-Web-To-App-HTTP rule denies source 10.20.1.0/24 from reaching TCP destination port 80 at priority 200.

Before you open the VM list, do you expect either machine to show a public IP address?

  • Return to the NextWork-Networking resource group.
  • Select Virtual machines.

You'll see vm-1 running with private address 10.20.1.4. You'll also see vm-2 running with private address 10.20.2.4.

Neither VM should show a public IP address. This proves the restored workloads are private before you inspect how they reach public endpoints.

Your secured two-tier lab is running again. Next, you'll capture the uncontrolled public address each private VM currently uses for outbound traffic.

Capture the Uncontrolled Egress Baseline

The restored lab now has two running Azure Virtual Machines with private addresses and no public IP resources. That private starting point gives you a clean way to reveal how outbound traffic leaves Azure.

Default outbound access can still give a private VM an implicit Microsoft-owned address for public connections. Capturing each value creates the before snapshot for your controlled egress change.

In this step, get ready to:
  • Confirm that each running VM has its expected private address.
  • Verify that both VMs have a blank public IP field.
  • Capture a separate implicit outbound IPv4 baseline from each VM.
Confirm each VM stays private

A VM's overview separates its private network address from its public IP assignment. A blank public IP field confirms that no dedicated public IP resource is attached.

  • Return to NextWork-Networking in the Azure portal from earlier.
  • Select vm-1.
  • On Overview, confirm that the private address shown is 10.20.1.4.
  • Confirm that Public IP address is blank.
  • Return to NextWork-Networking.
  • Select vm-2.
  • On Overview, confirm that the private address shown is 10.20.2.4.
  • Confirm that Public IP address is blank.

That's the starting condition confirmed: neither VM owns a public IP resource.

Capture vm-1's baseline

Action Run Command sends a shell script to the VM through its agent. This gives you the VM's outbound result without adding a public IP resource.

  • Return to NextWork-Networking.
  • Select vm-1.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Check vm-1's outbound IPv4 address by entering this custom script:
curl -s https://api.ipify.org

What Does This Check Prove?

The command asks api.ipify.org to return the public IPv4 address seen for the request. That value identifies the outbound address currently used by vm-1.

Before you select Run, do you think a VM with no public IP resource can still return a public address?

  • Select Run.

You'll see an IPv4 address in the command output. This proves that vm-1 can reach a public endpoint through an implicit outbound path.

  • Record the returned value for vm-1 here: vm-1 baseline IPv4 address.

No IPv4 Address in the Output?

Confirm that vm-1 still shows as running on its Overview page. Run Command relies on the VM agent plus outbound TCP 443 connectivity to Azure public IPs.

Help me diagnose missing RunShellScript output from vm-1. You can also share the non-sensitive error text in the NextWork community.

Capture vm-2's baseline

A second measurement shows how the implicit behavior applies to the other workload subnet. The returned values may match or differ because neither address belongs to a learner-controlled public IP resource.

  • Return to NextWork-Networking.
  • Select vm-2.
  • Select Run command under Operations.
  • Choose RunShellScript.

Before you run the second check, do you expect vm-2's value to match vm-1's baseline?

  • Check vm-2's outbound IPv4 address by entering the same custom script:
curl -s https://api.ipify.org

Why Repeat the Same Check?

Running the identical request on vm-2 gives you a like-for-like baseline. You can compare the outbound results without changing the test itself.

  • Select Run.

You'll see an IPv4 address in the output for vm-2. The successful response confirms that both private VMs currently have implicit outbound connectivity.

  • Record the returned value for vm-2 here: vm-2 baseline IPv4 address.
  • Label both recorded values as implicit Microsoft-managed outbound baselines.

The two baseline values may match or differ. Azure controls both addresses, so they are not stable egress identities that you manage.

Only One VM Returned an Address?

  • Confirm that vm-2 still shows as running on its Overview page.
  • Retry the same custom script in RunShellScript.

Help me compare the RunShellScript results from vm-1 and vm-2. You can also share the non-sensitive output in the NextWork community.

Your two implicit outbound baselines are recorded. Next, you'll give one subnet a controlled outbound address and observe the subnet-scoped difference.

Give One Subnet a Controlled Exit

Your baseline checks in the Azure portal proved that both private VMs can reach public endpoints through Microsoft-managed addresses. Those results also showed that you do not control the current outbound identity.

In this step, you will create a controlled outbound identity with Azure NAT Gateway. You will attach it to one subnet so a connection test can reveal the gateway's scope.

In this step, get ready to:
  • Create a static Standard V2 public IPv4 address for outbound traffic.
  • Create a Standard V2 NAT Gateway associated only with snet-app.
  • Compare the outbound results from vm-1 and vm-2.
Create the static egress address

A static public IPv4 address gives the NAT Gateway a predictable identity for internet-bound connections. Both the gateway and its compatible public IP use the Standard V2 SKU.

Charges Start with These Resources

Azure charges for the StandardV2 public IPv4 resource. NAT Gateway billing begins when the gateway is created.

The cleanup section shows you how to remove both resources when you finish.

  • Switch back to the Azure portal tab from your baseline check.
  • Select the search box at the top of the portal.
  • Enter Public IP address.
  • Select Public IP addresses from the search results.
  • Select Create on the Public IP addresses page.
  • Keep your active subscription in the Subscription field.
  • Select NextWork-Networking in the Resource group field.
  • Select Central US in the Region field.

Why Standard V2?

A StandardV2 NAT Gateway requires a compatible StandardV2 public IP address. The Regional tier provides the static IPv4 resource used by the gateway.

This pairing gives the selected subnet a stable outbound identity.

  • Enter public-ip-nat in the Name field.
  • Select IPv4 for IP version.
  • Select Standard V2 (For use with Standard V2 NAT Gateway) for SKU.
  • Select Regional for Tier.
  • Select Review + create.
  • Select Create after validation succeeds.
  • Return to the search box after the deployment finishes.
  • Enter Public IP address.
  • Select Public IP addresses from the search results.
  • Select public-ip-nat.
  • Find the assigned address beside IP address on the Overview page.
  • Record the assigned address here: your public-ip-nat IPv4 address.

Good progress. Your controlled outbound identity now has a static IPv4 address that you can compare against the VM results.

Create the subnet-scoped NAT Gateway

A NAT Gateway becomes the default next hop for new internet-bound connections from an associated subnet. The portal's Networking tab defines which subnet receives that path.

Why NAT Gateway for This Lab?

NAT Gateway provides a stable subnet-level outbound identity without extra route-table configuration. This keeps the lab focused on explicit egress control.

Azure Firewall adds traffic filtering and routing configuration. Standard Load Balancer outbound rules tie the egress path to a load-balancing design.

  • Return to the search box at the top of the portal.
  • Enter NAT gateway.
  • Select NAT gateways from the search results.
  • Select Create.
  • Keep your active subscription in the Subscription field.
  • Select NextWork-Networking in the Resource group field.
  • Enter nat-gateway in the NAT gateway name field.
  • Select Central US in the Region field.
  • Select Standard V2 for SKU.
  • Keep 4 in the TCP idle timeout (minutes) field.
  • Select Next.
  • Select + Add public IP addresses or prefixes on the Outbound IP tab.
  • Select Public IP addresses.
  • Select public-ip-nat.
  • Select Save.
  • Select the Networking tab.

The Networking tab controls the gateway's source scope. This configuration uses one existing subnet for the first test.

  • Select vnet-nextwork-lab in the Virtual network field.
  • Leave Default to all subnets unselected.
  • Select only snet-app under Select specific subnets.
  • Select Review + create.
  • Select Create after validation succeeds.
  • Return to the portal search box after the deployment completes.
  • Enter NAT gateway.
  • Select NAT gateways from the search results.
  • Select nat-gateway.
  • Select Networking.

You should see vnet-nextwork-lab with only snet-app listed as an associated subnet. The Default to all subnets option remains unselected.

That is the gateway live. It now has one static outbound IP and one associated source subnet.

Test the subnet boundary

The NAT association applies to new connections from its selected subnet. The same public IP check you used earlier can now expose that boundary.

Before you test, consider whether both VMs will use the recorded NAT address.

  • Return to the portal search box.
  • Enter Virtual machine.
  • Select Virtual machines from the search results.
  • Select vm-2.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Enter the outbound IP check by copying this script:
curl -s https://api.ipify.org

What Does This Command Do?

The command asks ipify to return the public IPv4 address used by the connection. The -s option hides progress details so the output contains only the address.

  • Select Run.

The output should be your public-ip-nat IPv4 address. This proves that new connections from vm-2 now leave through the NAT Gateway.

  • Return to Virtual machines.
  • Select vm-1.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Paste the same outbound IP check shown above.
  • Select Run.

The output should be a different IPv4 address from the one assigned to public-ip-nat. This split result is the intended shortfall.

What Did the Split Result Prove?

The gateway is associated only with snet-app. That association places vm-2 on the controlled NAT path.

The neighboring snet-web subnet has its own outbound scope. Its vm-1 workload remains outside this gateway association.

🙋‍♀️ Run Command Returned No Output?

Confirm that the affected VM still shows a running state. Run Command depends on the VM agent plus outbound TCP 443 connectivity to Azure public IPs.

Wait for the current RunShellScript operation to finish before starting another one.

Help me troubleshoot Azure RunShellScript returning no output.

  • Return to the Overview page for vm-1.
  • Confirm that Public IP address remains blank.
  • Switch to the Overview page for vm-2.
  • Confirm that Public IP address remains blank.

You now have a visible before-and-after result without assigning a public IP directly to either VM. Only the workload inside snet-app uses the controlled egress address.

You have proved that a NAT Gateway association controls one subnet at a time. Next, you will bring snet-web onto the same explicit outbound path.

Bring Both Tiers Under the Same Egress IP

Your last test exposed the subnet-scoped egress gap. vm-2 used public-ip-nat. vm-1 returned a different address.

Azure NAT Gateway controls outbound connections only for its associated subnets. You will add snet-web to nat-gateway so both tiers share the recorded static egress address.

In this step, get ready to:
  • Associate snet-web with nat-gateway.
  • Confirm that the gateway lists both workload subnets.
  • Compare the outbound address from both private VMs.
Associate the web subnet

The gateway already carries outbound connections for snet-app. Adding snet-web places the web tier on the same explicit outbound path.

  • Return to nat-gateway in the NextWork-Networking resource group.
  • Select Networking in the left menu.
  • Select vnet-nextwork-lab in the virtual network list.
  • Keep snet-app selected.
  • Select snet-web.
  • Keep Default to all subnets unselected.
  • Select Save to update the association.

Can't save the subnet association?

  • Confirm that snet-app belongs to vnet-nextwork-lab.
  • Confirm that snet-web belongs to vnet-nextwork-lab.
  • Ask your subscription administrator to confirm that your account can manage NAT Gateway subnet associations.

Help me troubleshoot an Azure NAT Gateway subnet association that will not save.

Confirm both subnet associations

The gateway's Networking view shows the scope of its outbound path. Checking that view confirms that the saved configuration covers both tiers.

  • Refresh the browser page.
  • Confirm that the vnet-nextwork-lab association lists snet-app.
  • Confirm that the vnet-nextwork-lab association lists snet-web.

That closes the configuration gap. The gateway now covers both workload subnets.

Test both egress paths

The updated association applies to new internet-bound connections. A fresh IP lookup now tests the outbound path from each subnet.

  • Return to vm-1 in the NextWork-Networking resource group.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Enter the following script in the script field:
curl -s https://api.ipify.org

What does this command check?

The command asks api.ipify.org to return the public IPv4 address used by the caller.

This fresh request tests the preferred outbound method for snet-web after its new gateway association.

Before you run the first check, do you expect the two VMs to report one shared address or different addresses?

  • Select Run.

The vm-1 output should now match the recorded static IPv4 address for public-ip-nat.

  • Return to vm-2 in the NextWork-Networking resource group.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Paste the same script into the script field.
  • Select Run.
  • Compare the vm-1 output with the recorded public-ip-nat address.
  • Compare the vm-2 output with the recorded public-ip-nat address.

You have closed the subnet-scoped egress gap. Both outputs now exactly match the static IPv4 address assigned to public-ip-nat.

Don't see two matching addresses?

  • Return to the nat-gateway Networking view to confirm that both subnets remain listed.
  • Rerun the lookup after the association is saved so the test opens a new connection.
  • Confirm that each VM is still running if Run Command does not return output.

Missing output can mean Run Command cannot reach Azure over outbound TCP 443.

Help me diagnose why my two private VMs do not return the same NAT Gateway public IPv4 address.

Both tiers now share one controlled outbound identity. Next, you will disable implicit default outbound access while keeping this explicit NAT path working.

Disable Default Outbound Access

Both workloads now send new outbound connections through the same Azure NAT Gateway public address. The final hardening move removes their remaining implicit outbound assignments.

Both subnets still allow Default outbound access. The Azure portal setting disables that fallback.

The existing NAT association remains the explicit path for internet-bound traffic.

In this step, get ready to:
  • Disable Default outbound access on both workload subnets.
  • Restart both private virtual machines after deallocation.
  • Verify that both VMs still use the NAT Gateway public IPv4 address.
Disable default outbound access

A subnet setting controls whether Azure can assign an implicit outbound address. Your explicit NAT association already provides the intended exit.

This network change can feel risky. Both subnets already have a NAT association that preserves their explicit route.

  • Switch back to vnet-nextwork-lab in the Azure portal from earlier.
  • Select Subnets.
  • Select snet-web from the subnet list.
  • Set Default outbound access to Disabled.
  • Select Save.

The snet-web details now show Default outbound access set to Disabled.

  • Select snet-app from the subnet list.
  • Set Default outbound access to Disabled.
  • Select Save.

The snet-app details now show the same Disabled setting.

What Does Disabled Change?

Default outbound access can assign a Microsoft-owned outbound IP when no explicit method exists. That address is not configurable.

The address can change without notice. Disabling the setting removes that implicit behavior.

Both subnets already use nat-gateway. Their explicit outbound path remains in place.

Only the Default outbound access setting changes. The existing network security group associations remain in place.

🙋‍♀️ Having Trouble Saving the Subnet?

Check that you selected snet-web or snet-app inside vnet-nextwork-lab.

Confirm that nat-gateway remains associated with the subnet before retrying the save.

Help me troubleshoot the subnet setting.

Restart both virtual machines

Existing Azure Virtual Machines need a deallocation cycle before the private subnet setting reaches their network interfaces. This cycle applies the subnet change to both workloads.

Deallocation briefly interrupts both lab machines. The short downtime lets the new subnet state take effect.

  • Return to vm-1 in the Azure portal.
  • Select Stop from the resource toolbar.
  • Confirm the stop if the portal asks.
  • Wait until vm-1 shows Stopped (deallocated).
  • Return to vm-2 in the Azure portal.
  • Select Stop from the resource toolbar.
  • Confirm the stop if the portal asks.
  • Wait until vm-2 shows Stopped (deallocated).

That is the interruption handled. Both machines are now fully deallocated.

  • Return to vm-1.
  • Select Start.
  • Wait until vm-1 shows Running.
  • Return to vm-2.
  • Select Start.
  • Wait until vm-2 shows Running.

Your lab is back online with both VMs showing Running.

VM Stuck While Restarting?

Wait until each VM reaches Stopped (deallocated) before selecting Start. Starting a VM too early can leave the lifecycle action pending.

Help me troubleshoot the VM restart.

Verify explicit NAT egress

A private subnet needs an explicit outbound method for public connections. The final test proves that the NAT path survived the removal of the implicit fallback.

  • Return to vm-1.
  • Select Run command under Operations.
  • Choose RunShellScript.

Before you run this check, predict whether removing the implicit fallback changes the public IPv4 address:

  • Test vm-1's outbound path by entering this command:
curl -s https://api.ipify.org

What Does This Command Prove?

The curl command makes an HTTPS request to api.ipify.org. The service returns the caller's public IPv4 address as text.

A match with the recorded public-ip-nat address proves that the new connection left through the gateway.

  • Select Run.

You will see one IPv4 address in the output. It exactly matches the recorded public-ip-nat address.

Run Command Not Returning Output?

Confirm that the VM shows Running before trying the command again. Run Command needs the VM agent plus outbound connectivity on TCP 443 to return results.

Help me troubleshoot Run Command.

  • Return to vm-2.
  • Select Run command under Operations.
  • Choose RunShellScript.
  • Enter the same IP check used on vm-1.
  • Select Run.

The second output also exactly matches the recorded public-ip-nat address.

What Have You Proved?

Both VM outputs match the NAT Gateway's static public IPv4 address. Explicit egress remains available after the subnet hardening change.

The matching results also prove that both workload subnets still use the shared gateway.

  • Return to vm-1.
  • Confirm that vm-1 still has no public IP resource.
  • Return to vm-2.
  • Confirm that vm-2 still has no public IP resource.

Both VM pages still show no public IP resource. Their internet-facing identity comes from the NAT Gateway.

  • Return to vnet-nextwork-lab.
  • Select Subnets.
  • Select snet-web.
  • Confirm that Default outbound access shows Disabled.
  • Return to Subnets.
  • Select snet-app.
  • Confirm that Default outbound access shows Disabled.

You have closed the implicit fallback while both private VMs keep reaching public endpoints through one controlled NAT address.

Secret mission

Prove the Gateway Carried Traffic

Your matching public IP proves the outbound route. Generate traffic from both private VMs. Then use Azure Monitor to show that nat-gateway processed it.

Clean Up Your Resources

Clean Up Your Resources

Your Azure lab can continue generating usage charges while its resources remain deployed. You can control that cost by choosing to keep everything running, pause the workloads for later, or delete the lab entirely.

Cost warning

Billing for an Azure NAT Gateway begins when the resource is created. Its hourly billing continues even without traffic.

Outbound data processing can add charges. Bandwidth can add separate charges.

Stopping the virtual machines does not stop NAT Gateway billing. Delete nat-gateway to end its hourly resource charges.

Resources you used:

  • The resource group NextWork-Networking in Central US.
  • The Azure Virtual Network named vnet-nextwork-lab with subnets snet-web and snet-app.
  • The private Azure Virtual Machines named vm-1 and vm-2.
  • The network interfaces vm-1-nic and vm-2-nic with their managed OS disks.
  • The Azure Network Security Groups named nsg-lab, nsg-web, and nsg-app.
  • The Standard V2 NAT Gateway named nat-gateway.
  • The Standard V2 IPv4 resource named public-ip-nat.

Keep everything running

No action is needed. Choose this if you plan to start Part 5 immediately or continue testing controlled outbound access.

  • Leave vm-1 and vm-2 running.
  • Keep nat-gateway associated with snet-web and snet-app.
  • Monitor your Azure costs while the lab remains deployed.

Pause - I'll come back to this later

Pause VM compute to reduce charges while preserving the lab's network and disks. Remove the outbound resources to stop their ongoing resource charges.

Pause the VM compute:

  • Open vm-1 from NextWork-Networking in the Azure portal.
  • Select Stop on the Overview page.
  • Confirm the stop request.
  • Open vm-2 from NextWork-Networking.
  • Select Stop on the Overview page.
  • Confirm the stop request.
  • Wait until both VM status fields show Stopped (deallocated).

Detach the gateway:

  • Open nat-gateway from NextWork-Networking.
  • Select Networking.
  • Clear the association for snet-app.
  • Clear the association for snet-web.
  • Select Save.

Delete the outbound resources:

  • Return to the Overview page for nat-gateway.
  • Select Delete.
  • Confirm the deletion.
  • Open public-ip-nat from NextWork-Networking.
  • Select Delete.
  • Confirm the deletion.
  • Return to NextWork-Networking.
  • Confirm that nat-gateway and public-ip-nat are no longer listed.

Your VNet, NSGs, NICs, disks, and VM definitions remain available. The private subnets have no internet egress until you restore an explicit outbound method.

Delete - I don't want to use this again

Deleting an entire resource group can feel drastic. This option clears every project resource together once you have finished with the lab.

  • Open Resource groups in the Azure portal.
  • Select NextWork-Networking.
  • Select Delete resource group.
  • Enter NextWork-Networking when Azure asks you to confirm the resource group name.
  • Complete the deletion confirmation.
  • Refresh Resource groups after the deletion finishes.

You should no longer see NextWork-Networking in the resource group list. This confirms that the VMs, disks, networking resources, NAT Gateway, and public IP have been removed.

Nice Work!

Nice Work!

Outstanding work! Your hardened two-tier lab now gives both private workloads one controlled outbound identity through Azure NAT Gateway.

You've learned how to:

  • Restore the secured two-tier topology from one reusable Azure Resource Manager template.
  • Prove implicit outbound connectivity from private Azure Virtual Machines through a before-and-after egress test. Replace it with the predictable address held by public-ip-nat.
  • Expose the subnet-scoped egress gap by attaching nat-gateway only to snet-app. Close the gap by associating snet-web. Keep both VMs online after disabling Default outbound access.
  • Secret Mission: Generated repeat traffic from both VMs. Confirmed the shared gateway processed it through a nonzero Bytes metric in Azure Monitor.

Ready to quiz yourself?