"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.