Home / Azure

AzureCheat sheetBeginner

Is an NSG blocking it? Azure network security group troubleshooting cheat sheet

Commands and queries to find which NSG rule allowed or denied a flow (effective rules, IP flow verify, next hop and VNet flow logs) and to add the narrowest fix.

Deepak Bhardwaj7 min read

Applies toAzure Virtual Network NSGs, Network Watcher, VNet flow logs with traffic analytics, Azure CLI 2.x

"VM A can't reach VM B on port 1433." Before you open a ticket with the network team, use these commands to answer three questions: is the traffic routed where you think, does an NSG deny it, and which rule is it. Each step takes about a minute.

How NSG evaluation works

Fact Why it matters
NSGs can be attached to a subnet and a NIC. Both must allow the flow. A permissive NIC NSG doesn't help if the subnet NSG denies the traffic
Inbound: subnet NSG first, then NIC NSG. Outbound: NIC first, then subnet. Check both in the right direction
Rules are processed by priority, lowest number first. Processing stops at the first match. A broad deny at 200 overrides your allow at 300
Custom rule priorities run from 100 to 4096. Default rules sit at 65000+ and can't be deleted. You override defaults, you don't remove them
NSGs are stateful You don't need a rule for return traffic
Default inbound: AllowVnetInBound, AllowAzureLoadBalancerInBound, DenyAllInBound Anything from the internet is denied unless you allow it
Default outbound: AllowVnetOutBound, AllowInternetOutBound, DenyAllOutBound Outbound to the internet is allowed unless a rule or a firewall route blocks it

Step 1: Which NSGs apply?

# NSG on the NIC
az network nic show -g rg-app -n vm-app-01-nic --query "networkSecurityGroup.id" -o tsv

# NSG on the subnet
az network vnet subnet show -g rg-net --vnet-name vnet-spoke-app -n snet-app \
  --query "networkSecurityGroup.id" -o tsv

# Rules in one NSG, including the defaults
az network nsg rule list -g rg-net --nsg-name nsg-snet-app --include-default -o table

Step 2: Effective rules (what's actually applied)

This merges the subnet and NIC NSGs and expands service tags and application security groups into real prefixes. The VM must be running.

az network nic list-effective-nsg -g rg-app -n vm-app-01-nic -o json

Narrow the output to inbound rules for one port:

az network nic list-effective-nsg -g rg-app -n vm-app-01-nic \
  --query "value[].effectiveSecurityRules[?direction=='Inbound'].{name:name, prio:priority, access:access, ports:destinationPortRange, src:sourceAddressPrefix}" -o table

Step 3: IP flow verify (the answer in one line)

IP flow verify tells you whether a specific 5-tuple is allowed or denied at a VM's NIC, and which rule decided it:

# Inbound: can 10.2.0.5 reach this VM on 1433?
az network watcher test-ip-flow -g rg-app --vm vm-db-01 \
  --direction Inbound --protocol TCP \
  --local 10.1.0.4:1433 --remote 10.2.0.5:50000

# Outbound: can this VM reach an external API on 443?
az network watcher test-ip-flow -g rg-app --vm vm-app-01 \
  --direction Outbound --protocol TCP \
  --local 10.1.1.4:50000 --remote 203.0.113.10:443

The output contains access (Allow or Deny) and ruleName, for example securityRules/Deny-All-Inbound or defaultSecurityRules/DenyAllInBound.

Tip

Run IP flow verify on both ends: outbound on the source VM and inbound on the destination VM. A flow can be allowed out of A and still be denied into B.

Step 4: Is it even routed there?

If the NSGs allow the flow but it still fails, check the route. A UDR that sends traffic to a firewall or NVA means the drop may be happening there instead.

az network watcher show-next-hop -g rg-app --vm vm-app-01 \
  --source-ip 10.1.1.4 --dest-ip 10.2.0.5

az network nic show-effective-route-table -g rg-app -n vm-app-01-nic -o table
Next hop type Meaning
VnetLocal / VNetPeering Direct. NSGs are the main filter
VirtualAppliance Goes through a firewall or NVA. Check its logs as well
VirtualNetworkGateway Goes to VPN/ExpressRoute. Check on-prem firewalls
None Dropped by a route. Fix the UDR
Internet Leaves Azure directly

Step 5: End-to-end test

Connection troubleshoot sends real TCP probes from the source VM. It needs the Network Watcher agent VM extension on the source VM.

az network watcher test-connectivity -g rg-app \
  --source-resource vm-app-01 \
  --dest-address 10.2.0.5 --dest-port 1433 --protocol Tcp

If connectionStatus is Unreachable, look at the hops[].issues entries. They name the blocking NSG rule or the unhealthy hop.

Step 6: Prove it happened (VNet flow logs)

IP flow verify only tests the rules as they are now. To see what was actually allowed or denied an hour ago, you need flow logs. Use VNet flow logs: NSG flow logs are being retired, and Microsoft recommends VNet flow logs for new setups.

az provider register --namespace Microsoft.Insights

az network watcher flow-log create --location westeurope \
  -g rg-net --name fl-vnet-spoke-app \
  --vnet vnet-spoke-app \
  --storage-account stflowlogsweu01 \
  --traffic-analytics true --workspace law-platform-weu \
  --interval 10

The storage account must be Standard and in the same region as the VNet. With a 10-minute traffic analytics interval, expect the data to arrive in Log Analytics with some delay. Raw JSON is written to storage every minute.

Find denied flows to a destination:

NTANetAnalytics
| where TimeGenerated > ago(4h)
| where SubType == "FlowLog" and FlowStatus == "D"
| where DestIp == "10.2.0.5" and DestPort == 1433
| project TimeGenerated, SrcIp, DestIp, DestPort, L4Protocol, FlowDirection, AclGroup, AclRule, DeniedInFlows, DeniedOutFlows
| order by TimeGenerated desc

Top denied flows across the VNet, which is useful after a rule change:

NTANetAnalytics
| where TimeGenerated > ago(1h)
| where SubType == "FlowLog" and FlowStatus == "D"
| summarize Denied = sum(DeniedInFlows + DeniedOutFlows) by SrcIp, DestIp, DestPort, AclRule
| top 20 by Denied

AclRule is the rule that decided the flow, and AclGroup is the NSG.

Step 7: Who changed the NSG?

az monitor activity-log list -g rg-net --offset 24h \
  --query "[?contains(operationName.value, 'Microsoft.Network/networkSecurityGroups')].{time:eventTimestamp, op:operationName.value, who:caller, status:status.value}" \
  -o table

Common causes

Symptom Cause Fix
IP flow verify says DenyAllInBound No rule allows the source Add a specific allow rule (below)
Allowed on NIC NSG, still denied Subnet NSG denies it Check list-effective-nsg; fix the subnet NSG
Load balancer backends show unhealthy after a "deny all" rule Custom deny above 65001 blocks AzureLoadBalancer probes Add an allow for service tag AzureLoadBalancer at a lower priority number than the deny
Works by IP, fails after moving the VM Rule uses an ASG the new NIC isn't in Add the NIC to the ASG
Inbound allowed, still times out Guest OS firewall (Windows Firewall, firewalld, iptables) or service not listening Use az vm run-command to check the local firewall and ss -lnt / netstat -an
Outbound to the internet fails, NSG allows it UDR to Azure Firewall, or no outbound path (no NAT Gateway/public IP) Check show-next-hop and the firewall logs

The narrow fix

Allow only what's needed: specific source prefix, destination and port. Never use * from Internet.

az network nsg rule create -g rg-net --nsg-name nsg-snet-db \
  --name Allow-AppSubnet-SQL-1433 --priority 210 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-address-prefixes 10.1.1.0/24 --source-port-ranges '*' \
  --destination-address-prefixes 10.2.0.0/24 --destination-port-ranges 1433 \
  --description "CHG-12345 app tier to SQL"

Then run test-ip-flow again to confirm that ruleName is now your rule.

Caution

Don't add a temporary "allow any any" rule to "see if it's the NSG". Use IP flow verify instead. It answers the same question without exposing the subnet. If you must open RDP/SSH for a break-fix, use Azure Bastion or just-in-time VM access, not an inbound rule from Internet.

Prevent it next time

  • Enable VNet flow logs with traffic analytics on every production VNet, so you have evidence when someone asks.
  • Use application security groups and service tags instead of hard-coded IPs, so rules survive redeployments.
  • Leave priority gaps (100, 200, 300…) so urgent rules can be inserted without renumbering.
  • Put a ticket or change ID in every rule's --description.
  • Manage NSGs as code, and alert on NSG write operations in the Activity log for production subscriptions.