A Lower-Priority NSG Allow Rule Silently Overrides Subnet-Level Deny
A lower-priority NSG allow rule attached to a network interface silently outranks a subnet-level deny rule, exposing a workload despite an apparently correct subnet policy.
Operational summary
At a glance
- Symptom
- A workload behind a subnet with an explicit deny rule for inbound traffic on port 3389 remains reachable from the internet.
- Likely cause
- A second NSG is associated directly with the virtual machine's network interface, separate from the subnet NSG.
- Impact
- The workload is reachable on RDP from any source IP address despite an apparently correct subnet-level deny policy, materially widening the network attack surface for brute-force and credential-stuffing…
- Verification signal
- Validation confirms the previously succeeding external connection is now refused and that legitimate maintenance access from the documented range still functions.
- Safe correction
- Correct the exposure by tightening the specific over-broad NIC-level allow rule rather than removing NSG associations wholesale, since removing an NSG association can have wider unintended effects on…
- Rollback or recovery
- If the narrowed rule blocks required maintenance access that was not fully captured before the change, restore the previous source address prefix value on the same rule ID…
Symptom
A workload behind a subnet with an explicit deny rule for inbound traffic on port 3389 remains reachable from the internet. The platform team confirms the subnet NSG shows a deny rule, yet connection attempts from an external test host succeed, and Azure Network Watcher’s IP flow verify tool reports the traffic as allowed rather than denied.
False Assumption
The team assumes that a single NSG attached to the subnet is the only enforcement point, and that a deny rule visible in the subnet NSG rule list is sufficient to block traffic reaching every network interface inside that subnet. This assumption ignores that Azure evaluates NSGs independently at both the subnet and the network interface (NIC) level when both are assigned, and that Azure merges the effective rule set for each NIC by priority number across whichever NSGs are associated with it, not by which NSG object the rule happens to live in.
Root Cause
A second NSG is associated directly with the virtual machine’s network interface, separate from the subnet NSG. This NIC-level NSG contains an allow rule with a lower priority number (200) permitting inbound TCP 3389 from a maintenance IP range that was, at some point, widened to Any during a troubleshooting session and never reverted. Because Azure evaluates effective security rules for a NIC using the combined, priority-ordered rule set from both the subnet NSG and the NIC NSG, and because lower priority numbers are evaluated first and NSGs process traffic on a first-match basis, the NIC-level allow rule at priority 200 wins over the subnet-level deny rule at priority 400. The subnet NSG’s deny rule is never reached for this traffic because a match already occurred.
Impact
The workload is reachable on RDP from any source IP address despite an apparently correct subnet-level deny policy, materially widening the network attack surface for brute-force and credential-stuffing attempts against a management port. The organisational assumption that subnet NSGs represent a single authoritative choke point is invalid whenever any NIC in that subnet also has its own NSG association, and this condition is easy to introduce during ad hoc troubleshooting and easy to miss during subsequent review because the subnet NSG rule list still displays the intended deny rule unchanged.
Diagnosis
Confirm the effective behaviour before changing anything. First, list every NSG associated with both the subnet and the specific NIC, because a NIC can have its own NSG in addition to, not instead of, the subnet NSG. Second, use Azure Network Watcher’s effective security rules view for the NIC, which merges and orders the subnet and NIC rule sets by priority and shows which single rule will match a given flow. Third, use IP flow verify with the actual source IP, destination port and protocol under test to observe the matched rule name and priority directly, rather than inferring behaviour from the rule list alone.
# Read-only: list NSGs associated with the subnet
az network vnet subnet show
--resource-group "rg-workload-prod"
--vnet-name "vnet-workload"
--name "snet-app"
--query "networkSecurityGroup.id" -o tsv
# Read-only: list the NSG associated directly with the NIC
az network nic show
--resource-group "rg-workload-prod"
--name "nic-vm-app01"
--query "networkSecurityGroup.id" -o tsv
# Read-only: view the merged effective security rules for the NIC
az network nic list-effective-nsg
--resource-group "rg-workload-prod"
--name "nic-vm-app01" -o table
# Read-only: verify actual flow disposition for the specific traffic under test
az network watcher test-ip-flow
--resource-group "rg-workload-prod"
--vm "vm-app01"
--direction Inbound
--protocol TCP
--local 10.20.1.5:3389
--remote 203.0.113.10:51000
The effective security rules output names the specific rule, its source NSG and its priority that determined the match. If a NIC-level rule with a lower priority number than the intended subnet deny rule appears as the match, that rule, not the subnet deny rule, is governing traffic for this NIC.
Correction
Correct the exposure by tightening the specific over-broad NIC-level allow rule rather than removing NSG associations wholesale, since removing an NSG association can have wider unintended effects on other traffic already relying on it. Narrow the source address prefix on the offending rule back to the documented maintenance range, or remove the rule entirely if no current business justification exists, and record the change against a specific rule ID and priority so the action is auditable.
# State-changing: narrow the over-broad NIC-level allow rule to the documented maintenance range
az network nsg rule update
--resource-group "rg-workload-prod"
--nsg-name "nsg-vm-app01-mgmt"
--name "Allow-RDP-Maintenance"
--priority 200
--source-address-prefixes "198.51.100.0/24"
--access Allow
Stop condition: if the maintenance range is undocumented or cannot be confirmed with the requesting team within the change window, do not guess a replacement range; instead set the rule’s access to Deny as an interim containment step and escalate for the correct scope, since an incorrect narrow range can silently break legitimate maintenance access while still leaving other exposure unexamined.
Validation
Validation confirms the previously succeeding external connection is now refused and that legitimate maintenance access from the documented range still functions. Re-run the IP flow verify command from the diagnosis step using the same external test source; the result must now report a deny disposition matching either the corrected NIC rule or the subnet deny rule. Separately, run IP flow verify with a source address inside the documented maintenance range to confirm that path still reports allow, so the correction has not silently blocked required access. Re-run the effective security rules listing to confirm the rule priority and scope shown match the intended, documented state.
Rollback
If the narrowed rule blocks required maintenance access that was not fully captured before the change, restore the previous source address prefix value on the same rule ID and priority rather than recreating the rule, since recreating it under a new priority can produce a different, unreviewed evaluation order.
# Rollback: restore prior source scope on the same rule if maintenance access breaks
az network nsg rule update
--resource-group "rg-workload-prod"
--nsg-name "nsg-vm-app01-mgmt"
--name "Allow-RDP-Maintenance"
--priority 200
--source-address-prefixes ""
--access Allow
Record the previous source-address-prefixes value before applying the correction so the rollback command above has a concrete value to restore; do not proceed with the correction if that prior value has not been captured.
Prevention
Treat subnet-level and NIC-level NSGs as two independent rule sources that Azure merges by priority, not as a layered override where the subnet always wins. Where practical, standardise on associating NSGs at the subnet level only and avoid NIC-level NSG associations unless a documented exception requires them, since a single enforcement point per subnet removes this class of silent override. Where NIC-level NSGs are required, require that any temporary widening made during troubleshooting carries an explicit expiry review, and include effective-security-rules review, not just rule-list review, in any change affecting network access controls.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
# Read-only: list NSGs associated with the subnet
az network vnet subnet show
--resource-group "rg-workload-prod"
--vnet-name "vnet-workload"
--name "snet-app"
--query "networkSecurityGroup.id" -o tsv
# Read-only: list the NSG associated directly with the NIC
az network nic show
--resource-group "rg-workload-prod"
--name "nic-vm-app01"
--query "networkSecurityGroup.id" -o tsv
# Read-only: view the merged effective security rules for the NIC
az network nic list-effective-nsg
--resource-group "rg-workload-prod"
--name "nic-vm-app01" -o table
# Read-only: verify actual flow disposition for the specific traffic under test
az network watcher test-ip-flow
--resource-group "rg-workload-prod"
--vm "vm-app01"
--direction Inbound
--protocol TCP
--local 10.20.1.5:3389
--remote 203.0.113.10:51000# State-changing: narrow the over-broad NIC-level allow rule to the documented maintenance range
az network nsg rule update
--resource-group "rg-workload-prod"
--nsg-name "nsg-vm-app01-mgmt"
--name "Allow-RDP-Maintenance"
--priority 200
--source-address-prefixes "198.51.100.0/24"
--access Allow# Rollback: restore prior source scope on the same rule if maintenance access breaks
az network nsg rule update
--resource-group "rg-workload-prod"
--nsg-name "nsg-vm-app01-mgmt"
--name "Allow-RDP-Maintenance"
--priority 200
--source-address-prefixes ""
--access AllowVerify, roll back or escalate
Verify
Validation confirms the previously succeeding external connection is now refused and that legitimate maintenance access from the documented range still functions.
Rollback
If the narrowed rule blocks required maintenance access that was not fully captured before the change, restore the previous source address prefix value on the same rule ID and priority rather than recreating the rule, since recreating it under a…
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.