Skip to main content
Azure · lesson 5 of 10
Azure intermediate Lesson 5 of 10

Also part of: Networking

Azure Virtual Network Basics

Build a VNet with two subnets, apply an NSG, then walk the exact debugging sequence for a VM that can't reach the internet.

By SkillByExample · Updated

azure networking virtual-network nsg

A virtual network (VNet) is an address space; subnets carve it into ranges that resources deploy into; NSGs attach to a subnet or a network interface and filter what passes. Almost every “my VM can’t talk to X” problem in Azure networking is a rule in one of these three layers, evaluated in an order that isn’t obvious until you’ve been burned by it once.

Building the network

az network vnet create \
  --resource-group learn-rg \
  --name learn-vnet \
  --address-prefix 10.1.0.0/16 \
  --subnet-name app-subnet \
  --subnet-prefix 10.1.1.0/24
$ az network vnet create --resource-group learn-rg --name learn-vnet --address-prefix 10.1.0.0/16 --subnet-name app-subnet --subnet-prefix 10.1.1.0/24
{
  "newVNet": {
    "addressSpace": {
      "addressPrefixes": ["10.1.0.0/16"]
    },
    "name": "learn-vnet",
    "provisioningState": "Succeeded",
    "subnets": [
      {
        "addressPrefix": "10.1.1.0/24",
        "name": "app-subnet"
      }
    ]
  }
}
az network vnet subnet create \
  --resource-group learn-rg \
  --vnet-name learn-vnet \
  --name data-subnet \
  --address-prefix 10.1.2.0/24
$ az network vnet subnet create --resource-group learn-rg --vnet-name learn-vnet --name data-subnet --address-prefix 10.1.2.0/24
{
  "addressPrefix": "10.1.2.0/24",
  "name": "data-subnet",
  "provisioningState": "Succeeded"
}

Two subnets, one VNet: app-subnet for VMs that need inbound access, data-subnet for resources that should never take inbound traffic directly. Splitting subnets by exposure level is what makes an NSG rule set legible later — one NSG per subnet, not one giant NSG covering everything.

Attaching an NSG that (deliberately) blocks outbound

az network nsg create --resource-group learn-rg --name app-nsg
$ az network nsg create --resource-group learn-rg --name app-nsg
{
  "NewNSG": {
    "name": "app-nsg",
    "provisioningState": "Succeeded",
    "securityRules": []
  }
}
az network nsg rule create \
  --resource-group learn-rg \
  --nsg-name app-nsg \
  --name deny-all-outbound \
  --priority 100 \
  --direction Outbound \
  --access Deny \
  --protocol "*" \
  --destination-port-ranges "*"

az network vnet subnet update \
  --resource-group learn-rg \
  --vnet-name learn-vnet \
  --name app-subnet \
  --network-security-group app-nsg
$ az network nsg rule create --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound --priority 100 --direction Outbound --access Deny --protocol "*" --destination-port-ranges "*"
{
  "access": "Deny",
  "direction": "Outbound",
  "name": "deny-all-outbound",
  "priority": 100,
  "protocol": "*"
}
$ az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --network-security-group app-nsg
{
  "name": "app-subnet",
  "networkSecurityGroup": {
    "id": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Network/networkSecurityGroups/app-nsg"
  }
}

Priority 100 beats the NSG’s built-in 65001 outbound-allow default rule, so this VM’s internet access is now blocked — set up on purpose to walk through diagnosing it.

The debugging sequence: a VM that can’t reach the internet

A VM deployed into app-subnet can’t curl out. First, confirm it really is a network problem and not a DNS or application one:

curl -m 5 https://example.com
$ curl -m 5 https://example.com
curl: (28) Failed to connect to example.com port 443 after 5001 ms: Connection timed out

A timeout, not “could not resolve host” — DNS resolved fine, the connection itself is being dropped somewhere. Next, check the NSG rules actually in effect for the VM’s network interface, not just what’s on the subnet:

az network nic list-effective-nsg --resource-group learn-rg --name learn-vmVMNic --output table
$ az network nic list-effective-nsg --resource-group learn-rg --name learn-vmVMNic --output table
NSG                    RuleName             Priority    Direction    Access
----------------------  -------------------  ----------  -----------  --------
app-nsg                 deny-all-outbound    100         Outbound     Deny
app-nsg                 AllowVnetOutBound    65000       Outbound     Allow
app-nsg                 AllowInternetOutBound  65001     Outbound     Allow

deny-all-outbound at priority 100 sits above both default allow rules and matches */* — it wins for every outbound packet regardless of destination. This is the finding: not a routing problem, not a missing public IP, but a deny rule shadowing the defaults.

az network nsg rule delete --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound
$ az network nsg rule delete --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound
curl -m 5 -o /dev/null -w "%{http_code}\n" https://example.com
$ curl -m 5 -o /dev/null -w "%{http_code}\n" https://example.com
200

Removing the one over-broad rule restored outbound access immediately — no VM restart, no route change needed, because NSG rule changes apply to new connections right away.

Practice

1. List all subnets in the VNet along with their address prefixes.
az network vnet subnet list --resource-group learn-rg --vnet-name learn-vnet --query "[].{Name:name, Prefix:addressPrefix}" --output table
$ az network vnet subnet list --resource-group learn-rg --vnet-name learn-vnet --query "[].{Name:name, Prefix:addressPrefix}" --output table
Name         Prefix
-----------  ------------
app-subnet   10.1.1.0/24
data-subnet  10.1.2.0/24

Reading subnet ranges before adding a new one avoids an overlapping-prefix error, which Azure rejects at creation time rather than allowing and routing ambiguously.

2. Add an NSG rule that allows outbound HTTPS specifically, instead of removing the deny-all rule.
az network nsg rule create \
  --resource-group learn-rg \
  --nsg-name app-nsg \
  --name allow-https-outbound \
  --priority 90 \
  --direction Outbound \
  --access Allow \
  --protocol Tcp \
  --destination-port-ranges 443
$ az network nsg rule create --resource-group learn-rg --nsg-name app-nsg --name allow-https-outbound --priority 90 --direction Outbound --access Allow --protocol Tcp --destination-port-ranges 443
{
  "access": "Allow",
  "direction": "Outbound",
  "name": "allow-https-outbound",
  "priority": 90,
  "protocol": "Tcp",
  "destinationPortRange": "443"
}

Priority 90 beats deny-all-outbound’s 100, so HTTPS specifically gets through while everything else stays blocked — a narrower fix than deleting the deny rule outright, useful when you want default-deny with named exceptions.

3. Peer two VNets and confirm the peering state.
az network vnet create --resource-group learn-rg --name learn-vnet-2 --address-prefix 10.2.0.0/16
az network vnet peering create --resource-group learn-rg --name learn-to-2 --vnet-name learn-vnet --remote-vnet learn-vnet-2 --allow-vnet-access true
az network vnet peering list --resource-group learn-rg --vnet-name learn-vnet --output table
$ az network vnet peering list --resource-group learn-rg --vnet-name learn-vnet --output table
Name        PeeringState    ProvisioningState
----------  --------------  -------------------
learn-to-2  Initiated       Succeeded

Initiated rather than Connected — peering requires a matching peering resource created on the other VNet too; only once both sides exist does the state flip to Connected and traffic actually flows.

4. Add a route table that sends all outbound traffic through a virtual appliance instead of directly to the internet.
az network route-table create --resource-group learn-rg --name learn-rt
az network route-table route create \
  --resource-group learn-rg \
  --route-table-name learn-rt \
  --name default-via-appliance \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address 10.1.1.10
az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --route-table learn-rt
$ az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --route-table learn-rt
{
  "name": "app-subnet",
  "routeTable": {
    "id": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Network/routeTables/learn-rt"
  }
}

This is the other common cause of “can’t reach the internet”: a user-defined route (UDR) overriding Azure’s default internet route with a next hop that points at an appliance which may not actually be forwarding traffic — worth checking alongside NSGs, not instead of them.

5. Check whether a specific port between two subnets is allowed, using Azure's built-in connectivity check rather than reasoning about rules by hand.
az network watcher test-ip-flow \
  --resource-group learn-rg \
  --vm learn-vm \
  --direction Outbound \
  --protocol TCP \
  --local 10.1.1.4:0 \
  --remote 10.1.2.5:5432
$ az network watcher test-ip-flow --resource-group learn-rg --vm learn-vm --direction Outbound --protocol TCP --local 10.1.1.4:0 --remote 10.1.2.5:5432
{
  "access": "Deny",
  "ruleName": "deny-all-outbound"
}

test-ip-flow simulates the exact packet through the current NSG rule set and reports which rule decided the outcome — the fast path to an answer instead of manually reading rule priorities against every layer.

Next: Azure SQL — provisioning a database, connecting to it, and a backup/restore walkthrough.

Frequently Asked Questions

What is the difference between a VNet, a subnet, and an NSG?
A virtual network (VNet) is an isolated network address space inside a region; a subnet is a range carved out of that address space that resources actually get deployed into; a network security group (NSG) is a set of allow/deny rules that can be attached to a subnet, a network interface, or both, and filters the traffic that reaches them. A VNet with no subnets can't hold resources, and a subnet with no NSG allows Azure's default rules only.
Why can't my VM reach the internet even though it has a public IP?
A public IP alone doesn't guarantee outbound connectivity — check, in order: whether the NSG on the subnet or NIC has an outbound deny rule with a lower priority number than the default allow, whether a route table (UDR) is redirecting 0.0.0.0/0 traffic somewhere else like a firewall appliance, and whether the OS-level firewall inside the VM is blocking the response. Most 'no internet' cases trace to one of the first two.
What are NSG default rules and can I delete them?
Every NSG ships with rules at priority 65000+ that allow traffic within the VNet, allow inbound from the Azure Load Balancer, and deny everything else inbound and allow everything else outbound. They can't be deleted, only overridden by rules you add at a lower (higher-priority) priority number, because a rule with a lower number always wins over one with a higher number.
What does a network security group's 'effective rules' view show that the rule list doesn't?
az network nic list-effective-nsg merges the rules from both the subnet-level and NIC-level NSGs (if both exist) into the single ordered list Azure actually evaluates for that network interface. Reading the two NSGs separately can miss a rule on the other one that's silently blocking or allowing traffic you didn't expect.