Azure / Guide 4
Network security groups and ASGs
NSG rules and priorities, subnet or NIC, service tags, application security groups, and checking what's actually applied.
Network security groups (NSGs) are the firewall every Azure virtual network already has. They're free, they're on by default in most deployments, and they're where most exposure problems start: one rule that allows RDP from anywhere, added "just for today" and never removed. Application security groups (ASGs) keep those rules readable as the estate grows.
How an NSG decides
An NSG is a list of rules. Each rule matches on five things (source, source port, destination, destination port and protocol) and either allows or denies. What matters day to day:
- Priority decides. Rules are processed from the lowest number (100) to the highest (4096), and processing stops at the first match. A deny at 200 beats an allow at 300.
- NSGs are stateful. If an inbound connection is allowed, the reply is allowed automatically. You don't write return rules.
- Changes affect new connections only. Removing a rule doesn't cut off a session that's already open.
- The default rules can't be deleted, only overridden with a lower-numbered rule of your own.
| Default rule | Priority | What it does |
|---|---|---|
AllowVNetInBound | 65000 | Allows everything from the virtual network, peered networks and connected on-premises ranges |
AllowAzureLoadBalancerInBound | 65001 | Allows load balancer health probes |
DenyAllInbound | 65500 | Denies everything else inbound |
AllowVnetOutBound | 65000 | Allows everything to the virtual network |
AllowInternetOutBound | 65001 | Allows everything to the internet |
DenyAllOutBound | 65500 | Denies everything else outbound |
Two things surprise people. First, AllowVNetInBound means every VM in the network (and every peered network, and on-premises over VPN) can reach every other VM on every port unless you say otherwise. Second, AllowInternetOutBound means everything can call out to the internet. Both are reasonable defaults for getting started; neither is a security posture.
Subnet or network interface?
An NSG can be associated with a subnet, a network interface (NIC), or both. When both are set, traffic has to be allowed by both:
- Inbound: the subnet NSG is checked first, then the NIC NSG.
- Outbound: the NIC NSG is checked first, then the subnet NSG.
Put NSGs on subnets, one per subnet, and avoid NIC-level NSGs unless there's a specific exception. Two layers of rules that both have to agree is the most common reason a "correct" rule doesn't work. Use ASGs (below) for per-server differences instead.
A few subnets are special:
GatewaySubnet(VPN and ExpressRoute gateways): don't associate an NSG.AzureFirewallSubnet: NSGs aren't supported; the firewall does the filtering.AzureBastionSubnet: an NSG is optional, but if you add one it must carry the specific rules Bastion needs (HTTPS in from the internet andGatewayManager, and the data-plane and outbound rules). Missing rules break the deployment.- Platform-managed services such as Azure SQL Managed Instance and some App Service integrations add their own rules. Check the service's documentation before tightening a subnet it's delegated to.
Service tags instead of IP lists
A service tag is a Microsoft-maintained group of IP ranges. Use them instead of typing addresses: Microsoft updates the ranges, and the rule still says what it means.
| Tag | Means |
|---|---|
Internet | Any address outside the virtual network that isn't Azure-internal |
VirtualNetwork | The virtual network, peered networks and connected on-premises ranges |
AzureLoadBalancer | Health probe source. Keep it allowed if you add your own deny-all rule |
Storage, Sql, AzureKeyVault, AzureMonitor | Those services' public endpoints. Add a region to narrow them, for example Storage.EastUS |
GatewayManager | Azure's management traffic for gateways and Bastion |
An augmented rule takes several ports or several IP ranges in one rule (80,443, or three office ranges). Fewer rules, easier review. A single rule can't mix several service tags, though; that needs one rule per tag.
# Which address ranges are behind a tag right now
$tags = Get-AzNetworkServiceTag -Location eastus
($tags.Values | Where-Object Name -eq 'Storage.EastUS').Properties.AddressPrefixes
Application security groups
An ASG is a label you put on a server's network interface: asg-web, asg-app, asg-db. NSG rules then refer to the label instead of IP addresses:
| Priority | Source | Destination | Port | Action |
|---|---|---|---|---|
| 100 | Internet | asg-web | 443 | Allow |
| 200 | asg-web | asg-app | 8443 | Allow |
| 300 | asg-app | asg-db | 1433 | Allow |
| 4000 | VirtualNetwork | VirtualNetwork | Any | Deny |
Add a fourth web server and it only needs the asg-web label: no rule changes, no IP address to look up. The rules also read like the architecture diagram, which makes access reviews quick.
What to know before you design around them:
- One virtual network per ASG. All network interfaces in an ASG must be in the same virtual network as the first one added. A rule that uses an ASG as both source and destination needs both ASGs in the same virtual network.
- A NIC can be in several ASGs (up to 20 per IP configuration), so a server can be both
asg-webandasg-monitored. - ASGs only work inside the network. For traffic from on-premises or the internet, the other side of the rule is still an IP range or a service tag.
- The deny rule at 4000 matters. Without it,
AllowVNetInBoundlets every server reach every other server and the ASG rules add nothing. Add the deny once the allow rules are proven.
$rg = 'rg-notes-test'; $loc = 'eastus'
# The labels
$asgWeb = New-AzApplicationSecurityGroup -ResourceGroupName $rg -Name 'asg-web' -Location $loc
$asgApp = New-AzApplicationSecurityGroup -ResourceGroupName $rg -Name 'asg-app' -Location $loc
# Rules that use them
$rules = @(
New-AzNetworkSecurityRuleConfig -Name 'Allow-Internet-Web-443' -Priority 100 -Direction Inbound -Access Allow `
-Protocol Tcp -SourceAddressPrefix Internet -SourcePortRange * `
-DestinationApplicationSecurityGroup $asgWeb -DestinationPortRange 443
New-AzNetworkSecurityRuleConfig -Name 'Allow-Web-App-8443' -Priority 200 -Direction Inbound -Access Allow `
-Protocol Tcp -SourceApplicationSecurityGroup $asgWeb -SourcePortRange * `
-DestinationApplicationSecurityGroup $asgApp -DestinationPortRange 8443
)
$nsg = New-AzNetworkSecurityGroup -ResourceGroupName $rg -Location $loc -Name 'nsg-notes-test-web' -SecurityRules $rules
# Associate the NSG with the subnet
$vnet = Get-AzVirtualNetwork -ResourceGroupName $rg -Name 'vnet-notes-test'
$subnet = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name 'snet-web'
$subnet.NetworkSecurityGroup = $nsg
$vnet | Set-AzVirtualNetwork | Out-Null
# Put a server's NIC in asg-web
$nic = Get-AzNetworkInterface -ResourceGroupName $rg -Name 'vm-notes-test-01-nic'
$nic.IpConfigurations[0].ApplicationSecurityGroups = @($asgWeb)
$nic | Set-AzNetworkInterface | Out-Null
Rules that cause trouble
These are the findings that come up in almost every review. Get-AzNsgRuleReport.ps1 looks for all of them across every subscription.
- RDP (3389) or SSH (22) open to
Internet,Anyor0.0.0.0/0. Internet-facing management ports are scanned within minutes of being opened. Use Bastion or just-in-time access instead (see Guide 5). - Database and remote-management ports open to the internet: 1433, 3306, 5432, 5985/5986 (WinRM) and similar.
- Allow Any to Any on any port. Often added while troubleshooting. It turns the NSG off.
- Port ranges wider than needed, such as
1-65535or3000-4000, which quietly include 3389. - Your own deny-all rule without an allow for
AzureLoadBalancerabove it. Load balancer health probes fail and the backend drops out of the pool. - Rules for servers that no longer exist, and NSGs attached to nothing.
Find-AzOrphanedResources.ps1lists unattached NSGs.
A numbering and naming convention
Agree on priority bands before there are a hundred rules, and leave gaps so a new rule fits without renumbering:
| Priorities | Used for |
|---|---|
| 100–999 | Explicit denies that must always win (for example block a known bad range) |
| 1000–2999 | Application allows, in steps of 10 |
| 3000–3999 | Shared services: monitoring, backup, domain controllers, Bastion |
| 4000–4096 | The catch-all denies that replace the permissive defaults |
Name rules Action-Source-Destination-Port, for example Allow-Web-App-8443, and use the rule's description for the change or ticket number. Name NSGs after what they protect: nsg-<workload>-<subnet>.
Check what is actually applied
When a connection fails, don't read the rules and guess. Ask Azure:
# Every rule that applies to a NIC, subnet and NIC NSGs combined (the VM must be running)
Get-AzEffectiveNetworkSecurityGroup -ResourceGroupName 'rg-notes-test' -NetworkInterfaceName 'vm-notes-test-01-nic' |
Select-Object -ExpandProperty EffectiveSecurityRules |
Select-Object Name, Direction, Priority, Access, Protocol, SourceAddressPrefix, DestinationPortRange
# Would this connection be allowed, and by which rule? (Network Watcher IP flow verify)
$vm = Get-AzVM -ResourceGroupName 'rg-notes-test' -Name 'vm-notes-test-01'
Test-AzNetworkWatcherIPFlow -Location eastus -TargetVirtualMachineId $vm.Id -Direction Inbound -Protocol TCP `
-LocalIPAddress 10.10.1.4 -LocalPort 3389 -RemoteIPAddress 203.0.113.10 -RemotePort 50000
IP flow verify returns Allow or Deny and the name of the rule that decided, which ends most "is it the NSG?" conversations in a minute.
For a record of what actually flowed, turn on virtual network flow logs in Network Watcher and send them to a storage account, with traffic analytics into your Log Analytics workspace. They replace NSG flow logs: new NSG flow logs can no longer be created, and existing ones retire on 30 September 2027, so migrate any you still have. One flow log per virtual network covers every subnet and NIC in it.