Networking Azure

Micro-Segmentation with NSGs and Application Security Groups: Tier Isolation at Scale

Most Azure networks segment at the subnet boundary: a web subnet, an app subnet, a data subnet, and Network Security Groups that allow 10.0.1.0/24 -> 10.0.2.0/24 on a port. It works until it doesn’t. The moment two workloads share a subnet, your NSG can’t tell them apart — they’re the same CIDR, so a rule that lets the payments API talk to the database also lets the marketing site talk to it. And every IP you hardcode becomes a maintenance liability the day autoscale hands out a new address. Micro-segmentation fixes both problems by moving the unit of policy from a subnet to a workload identity. Instead of “this address range may reach that address range,” you write “the web role may reach the app role on 443,” and Azure resolves the roles to whatever NICs currently wear them.

This is an Advanced walkthrough, and it is deliberately exhaustive. We build the model on Azure end to end: the Network Security Group (its rule anatomy, the five-tuple, priorities, the non-deletable default rules, service tags, augmented rules, and how a packet is actually evaluated), Application Security Groups (ASGs — tag-based grouping that kills IP sprawl), the web/app/data tier-isolation pattern, the difference between an NSG on a subnet versus a NIC and how the two stack, effective security rules, the deny-by-default design that shadows the permissive platform defaults, how NSGs interact with private endpoints, when to layer Azure Firewall in the hub versus rely on NSGs at the edge, and how to prove the whole thing with NSG/VNet flow logs and IP Flow Verify. Every mechanism gets a table that enumerates the options, defaults, limits and gotchas, plus real az and Bicep you can lift. We close by mapping the Azure model onto AWS security groups and NACLs so the mental model transfers, and by walking a complete worked 3-tier design.

By the end you will stop describing the network and start describing the application. You will know exactly which construct to reach for — an ASG, a service tag, an augmented rule, a hub firewall — for each flow, why the 65000 AllowVnetInBound default is the quiet enemy of segmentation, and how to keep a default-deny posture intact fleet-wide with Azure Policy instead of watching it erode within a quarter.

What problem this solves

A flat virtual network is a single blast radius. When every VM can reach every other VM on every port — which is the default on Azure, because AllowVnetInBound at priority 65000 permits all intra-VNet traffic — one compromised host is a beachhead into all of them. Lateral movement, the attacker technique of pivoting from a foothold to its neighbours, happens inside the subnet, exactly where a CIDR-based rule is blind. Subnet-coarse NSGs give you a comforting diagram with three boxes and arrows, and none of the isolation the diagram implies.

The three ways subnet-level NSGs fail, and they fail quietly:

Who hits this: any team past the toy stage on Azure. Regulated workloads (PCI, HIPAA, SOC 2) whose auditors require that a sensitive tier be reachable only from a named upstream. Dense environments where cost pressure packs unrelated services into shared subnets. Platform teams running landing zones where dozens of app teams write into shared NSGs and step on each other’s priority numbers. The payoff of micro-segmentation is not “more firewalls” — it is a default-deny posture where every allowed flow is named, intentional, and tied to a workload role rather than an address.

To frame the whole field before the deep dive, here is the shift in one table — the same three-tier app, described the old way and the new way:

Concern Subnet-CIDR NSG (the old way) ASG micro-segmentation (this article)
Unit of policy An address range (10.0.2.0/24) A workload role (asg-app)
Same-subnet isolation Impossible — same CIDR Native — roles differ even in one subnet
Autoscale / re-image Breaks or forces a wider CIDR Transparent — new NIC inherits the tag
Rule count at scale Trends to N×M, unreadable One rule per flow, readable as a sentence
Default posture Flat (65000 allows all east-west) Default-deny (your rule shadows 65000)
Auditability “What does this /24 permit?” — unknown “web→app:443” — self-documenting
Cross-VNet reach The only tool you have Falls back to service tags / firewall

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand Azure networking fundamentals: a virtual network (VNet) with subnets, that a NIC attaches a VM to a subnet and carries one or more IP configurations, and that DHCP inside a VNet hands out addresses from the subnet range. You should be comfortable running az in Cloud Shell, reading JSON output, and authoring Bicep. Familiarity with TCP/UDP ports, CIDR notation, and the idea of stateful versus stateless firewalls will make the AWS comparison land. If any of that is shaky, start with Azure Virtual Network, Subnets and NSGs: Networking Fundamentals and the Azure NSG Rules Explained: Priorities, Default Rules and Augmented Security Rules for Beginners before this one.

This article sits in the Networking / security-of-the-network track and is the deep, design-grade sibling of two more focused pieces: Application Security Groups (ASG): Tag-Based Microsegmentation Without IP Sprawl covers the ASG construct in isolation, and Why Is My Traffic Blocked? Debugging NSGs with Effective Rules and IP Flow Verify is the troubleshooting companion. Micro-segmentation is the network pillar of a broader strategy — see Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars — and it lives inside a topology decision: whether you route east-west through a hub is the subject of Hub-and-Spoke vs Virtual WAN: Choosing an Enterprise Cloud Network Topology.

A quick map of who owns what during a segmentation project, so you pull in the right person:

Layer What lives here Usual owner What it can break
Subnet / VNet design Address plan, subnet sizing Network / platform ASG same-VNet constraint; PE subnet
NSG rules Allows, denies, priorities Platform (floor/ceiling) + app teams (middle) East-west isolation, accidental exposure
ASG membership NIC ↔ role tagging App team (via IaC) Ungoverned NIC = unintended reach
Azure Policy Guardrails, drift control Governance / platform Missing NSG, Internet mgmt ports
Azure Firewall Cross-VNet / egress inspection Network / security Over-centralisation, cost, latency
Flow logs Validation, audit evidence Security / SecOps Blind spots if not enabled

Core concepts

Six mental models make every later decision obvious.

An NSG is an ordered list of allow/deny rules, evaluated by priority, first match wins. A Network Security Group (Microsoft.Network/networkSecurityGroups) is a stateful packet filter you attach to a subnet or a NIC. It holds security rules, each a five-tuple — source, source port, destination, destination port, protocol — plus a direction (inbound/outbound), an access (Allow/Deny), and a priority (100–4096, lower evaluated first). Inbound and outbound are two separate ordered lists. For a given packet in a given direction, Azure walks the rules from lowest priority number upward; the first rule that matches decides, and evaluation stops. Because it is stateful, a permitted inbound flow’s return traffic is allowed automatically — you do not write a matching outbound rule for the reply.

Every NSG ships with non-deletable default rules — and one of them is the enemy. Three inbound and three outbound defaults exist at priorities 65000/65001/65500. The dangerous one is AllowVnetInBound at 65000: by default any VM in the VNet can reach any other VM on any port. Micro-segmentation is the discipline of shadowing that default with your own lower-numbered rules — explicit allows for sanctioned flows, then an explicit east-west deny that fires before 65000 can.

An ASG is a workload label you attach to NICs, usable as a source or destination in a rule. An Application Security Group (Microsoft.Network/applicationSecurityGroups) is a logical name — asg-web, asg-app, asg-data — that you associate with a NIC’s IP configuration. In a rule you write source = asg-web instead of source = 10.0.1.0/24, and Azure resolves the membership to whatever NICs currently carry the label, including ones autoscale created seconds ago. This is what lets a rule read like a sentence about the application and lets you isolate two workloads that happen to share a subnet.

Service tags are Microsoft-managed CIDR sets you reference by name. A service tag like Internet, VirtualNetwork, AzureLoadBalancer, Storage.eastus2, Sql, AzureKeyVault, or AzureCloud stands for a set of prefixes Microsoft maintains and updates for you. You use them where an ASG can’t reach — cross-VNet, egress to a PaaS service, or admitting the load balancer’s health probes — without hardcoding addresses that Microsoft changes underneath you.

Augmented rules pack many prefixes, ports, and ASGs into one rule. On the Resource Manager stack you can list multiple source/destination prefixes, multiple port ranges, and multiple ASGs in a single rule (subject to limits). This is what keeps a real rule set small: one “web tier ingress” rule instead of a dozen near-duplicates.

Where you attach the NSG changes its blast radius; two attached NSGs both apply. An NSG on a subnet governs every NIC in it; an NSG on a NIC governs just that NIC. You may have both. For traffic into a NIC, the subnet NSG’s inbound rules evaluate, then the NIC NSG’s inbound rules — both must allow. For traffic out, the NIC NSG’s outbound rules, then the subnet NSG’s. If either denies, the packet drops. The effective security rules view shows the merged, resolved outcome — the source of truth when reasoning gets hard.

The five-tuple and rule anatomy in one table

Pin down every field of a security rule before the deep sections. This is the vocabulary the rest of the article assumes:

Field What it holds Values you’ll use Gotcha
Name Human label Allow-Web-To-App-443 Name like a sentence; it’s the only in-place docs
Priority Order 100–4096 (lower first) Must be unique per direction per NSG
Direction Inbound / Outbound one or the other Two independent ordered lists
Access Allow / Deny Allow or Deny First match wins, then evaluation stops
Protocol Tcp / Udp / Icmp / * usually Tcp or * * = any; Esp/Ah also valid
Source Origin CIDR, service tag, or ASG(s) Prefix or ASG in one field, not both
Source port Ephemeral range almost always * Pinning source ports is a common mistake
Destination Target CIDR, service tag, or ASG(s) Same prefix-or-ASG rule as source
Destination port Listening port(s) 443, 1433, 7000, ranges Augmented: multiple ranges allowed

The default rules you cannot delete

Every NSG contains these six. You cannot delete or reorder them; you override them with lower priority numbers. Memorise which one you must shadow:

Direction Priority Name Source → Destination Access Why it matters
Inbound 65000 AllowVnetInBound VirtualNetwork → VirtualNetwork Allow The enemy — flat east-west by default
Inbound 65001 AllowAzureLoadBalancerInBound AzureLoadBalancer → Any Allow Health probes; don’t accidentally deny it
Inbound 65500 DenyAllInBound Any → Any Deny Backstop for non-VNet sources
Outbound 65000 AllowVnetOutBound VirtualNetwork → VirtualNetwork Allow East-west egress open by default
Outbound 65001 AllowInternetOutBound Any → Internet Allow Egress to Internet open by default
Outbound 65500 DenyAllOutBound Any → Any Deny Backstop for anything not Internet/VNet

The whole game: your explicit allows sit at low numbers (say 1000–1010), your east-west deny sits above the 65000 defaults (say 4000), and the permissive AllowVnetInBound never gets reached for unsanctioned flows.

The NSG rule model in depth

Priorities, ranges, and first-match-wins

Usable priorities run 100 to 4096. Lower number = evaluated first; first match wins and stops evaluation. This single fact drives every design decision. A Deny at priority 200 beats an Allow at 1000, which is precisely how you make a control non-negotiable. Two allows can never “combine” — the lower-numbered one decides and the other is dead code for that packet.

Leave gaps. Numbering rules 100, 200, 300 (not 100, 101, 102) lets you slot a new rule between existing ones without renumbering the whole set. The practical numbers and limits:

Property Value / limit Note
Priority range (usable) 100–4096 65000–65500 reserved for defaults
Uniqueness Per NSG, per direction Same number may exist inbound & outbound
Evaluation Lowest number first First match wins; stop
Recommended spacing 100 apart Room to insert later
Default rules 65000 / 65001 / 65500 Non-deletable, non-editable
Rules per NSG 1,000 (default) Raisable via support to 5,000
NSGs per region per subscription 5,000 (default) Raisable
Source/dest ASGs per rule 10 (each side) Augmented-rule limit
Prefixes/ports per rule Bounded (hundreds) Augmented rules; verify current limit before packing

The stateful nature of NSGs

NSGs are stateful. When an inbound rule permits a TCP flow, the connection is tracked and the return packets are allowed automatically — you do not need a symmetric outbound allow for the reply. This is the single biggest difference from AWS NACLs (which are stateless) and it changes how many rules you write: you author rules for the initiating direction of a flow, not both directions. Two caveats bite people — statefulness covers replies to inbound, not new outbound connections (a flow your app initiates, like dialling a database, still needs its own outbound allow under an egress lockdown), and removing an allow blocks new connections immediately but may not tear down a long-lived session already established under it, so plan cutovers for connection churn.

Behaviour NSG (stateful) Practical implication
Inbound allow → return traffic Auto-allowed No symmetric outbound rule needed for replies
Outbound-initiated flow Needs its own outbound allow Egress lockdown requires explicit allows
Rule removed mid-session New conns blocked; some existing may persist Plan cutovers; don’t assume instant teardown
Port to match Destination port of the listener Source port is ephemeral — leave it *

Service tags: named prefix sets you don’t maintain

A service tag represents a set of IP prefixes that Microsoft maintains, so you reference Storage.eastus2 instead of pasting a block of CIDRs that changes every month. Tags are the correct tool for anything an ASG can’t express — Internet, cross-VNet, or Azure PaaS egress. The ones you reach for constantly:

Service tag Represents Typical use Direction
VirtualNetwork The VNet + peered + connected on-prem space East-west allows/denies Both
Internet Public IP space outside Azure Deny inbound mgmt; scoped egress Both
AzureLoadBalancer The platform LB probe source Must allow for health probes Inbound
Storage / Storage.<region> Azure Storage endpoints Egress to blob/file, regionally scoped Outbound
Sql / Sql.<region> Azure SQL / Synapse gateways Egress to Azure SQL Outbound
AzureKeyVault / .<region> Key Vault endpoints Egress for KV references Outbound
AzureActiveDirectory Entra ID auth endpoints Egress for token acquisition Outbound
AzureCloud / .<region> All Azure public IPs (regional) Broad Azure egress allow Outbound
AzureMonitor Monitor / Log Analytics ingestion Egress for agents/telemetry Outbound
GatewayManager / AzurePlatformDNS Platform control channels Rarely denied; know they exist Inbound/Out

Regional variants (Storage.eastus2) are tighter than the global tag and should be preferred — they shrink the permitted set to the region you actually talk to. Use az network list-service-tags --location eastus2 to dump the current tag-to-prefix mapping when you need to reason about exactly what a tag covers.

Augmented security rules: many-to-many in one rule

On the Resource Manager stack, a single rule can carry multiple source prefixes, multiple destination prefixes, multiple port ranges, and multiple ASGs. This “augmented” capability is what keeps a real NSG from ballooning: one rule expresses “these three admin subnets may reach the app ASG on 22, 3389, and 5986” instead of nine rules.

# One augmented rule: two admin prefixes -> app ASG on three mgmt ports
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Allow-Admin-To-App-Mgmt --priority 700 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-address-prefixes 10.0.100.0/27 10.0.101.0/27 \
  --destination-asgs asg-app \
  --destination-port-ranges 22 3389 5986 --source-port-ranges '*'

Two constraints to internalise: within a single rule’s source (or destination), you specify ASGs or address prefixes, not both; and the number of ASGs per side is capped (10). If you need an IP-based source and an ASG-based source for the same logical flow, that is two rules.

Augmentation Allowed in one rule? Limit If you exceed it
Multiple source prefixes Yes Bounded (hundreds) Split into two rules
Multiple destination prefixes Yes Bounded Split
Multiple port ranges Yes Bounded Split
Multiple source ASGs Yes 10 Add another rule / regroup
Multiple destination ASGs Yes 10 Add another rule / regroup
ASGs and prefixes in same field No Two rules

Application Security Groups as workload identity

Creating ASGs that mirror roles, not topology

Create ASGs that name what a workload is, not where it lives. The three-tier baseline is asg-web, asg-app, asg-data, but real environments add cross-cutting roles — asg-patch-managed, asg-bastion-reachable, asg-monitored — that a NIC can carry in addition to its tier.

RG=rg-app-prod
LOC=eastus2

for tier in web app data; do
  az network asg create \
    --resource-group "$RG" --name "asg-${tier}" --location "$LOC"
done
resource asgWeb 'Microsoft.Network/applicationSecurityGroups@2023-11-01' = {
  name: 'asg-web'
  location: location
}
resource asgApp 'Microsoft.Network/applicationSecurityGroups@2023-11-01' = {
  name: 'asg-app'
  location: location
}
resource asgData 'Microsoft.Network/applicationSecurityGroups@2023-11-01' = {
  name: 'asg-data'
  location: location
}

Attaching NICs, IP configs, and VMSS instances

An ASG is associated at the IP configuration level of a NIC. A NIC can belong to several ASGs at once — that’s how you express overlapping roles:

# Associate an existing NIC's ipconfig with one or more ASGs
az network nic ip-config update \
  --resource-group "$RG" \
  --nic-name nic-web-vm01 --name ipconfig1 \
  --application-security-groups asg-web asg-patch-managed

For VM Scale Sets, set the ASG in the network profile so every instance is born into the right group — no per-instance step, no IP to chase:

az vmss update \
  --resource-group "$RG" --name vmss-web \
  --set virtualMachineProfile.networkProfile.networkInterfaceConfigurations[0]\
.ipConfigurations[0].applicationSecurityGroups='[{"id":"/subscriptions/<sub>/resourceGroups/'"$RG"'/providers/Microsoft.Network/applicationSecurityGroups/asg-web"}]'
# Roll instances so the new model takes effect
az vmss update-instances --resource-group "$RG" --name vmss-web --instance-ids '*'
// In a VM's NIC definition — the instant it boots it's governed
resource nic 'Microsoft.Network/networkInterfaces@2023-11-01' = {
  name: 'nic-app-vm01'
  location: location
  properties: {
    ipConfigurations: [ {
      name: 'ipconfig1'
      properties: {
        subnet: { id: appSubnet.id }
        privateIPAllocationMethod: 'Dynamic'
        applicationSecurityGroups: [ { id: asgApp.id } ]
      }
    } ]
  }
}

The constraints that bound the construct

Two hard rules define where ASGs work and where you fall back to tags or firewall:

  1. Same region. An ASG and every NIC referencing it must live in the same Azure region.
  2. Same VNet per rule. Within a single NSG rule, all NICs referenced by the source ASG(s) and destination ASG(s) must belong to the same virtual network. ASGs are an intra-VNet construct. For cross-VNet flows you fall back to service tags, IP ranges, or inspection at a hub firewall.
ASG property / limit Value Design consequence
Scope Region + single VNet per rule Intra-VNet segmentation only
ASGs a NIC can join Multiple Model overlapping roles
ASGs per NSG rule side 10 Regroup if a rule needs more
Membership resolution Dynamic, at evaluation Autoscale/re-image transparent
Cross-VNet source/dest Not supported in one rule Use service tags or hub firewall
Cross-region Not supported One ASG set per region
Works with subnet or NIC NSG Yes (rules reference ASGs) Attach NSG where blast radius fits

Designing a deny-by-default baseline

Shadowing the 65000 AllowVnetInBound default

The baseline is built bottom-up: explicit allows at low numbers, then an intra-VNet catch-all deny that sits above the 65000 default and below your allows. First, the sanctioned flows between named ASGs:

NSG=nsg-app-tier

# web -> app on 443
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Allow-Web-To-App-443 --priority 1000 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-asgs asg-web --destination-asgs asg-app \
  --destination-port-ranges 443 --source-port-ranges '*'

# app -> data on 1433 (SQL)
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Allow-App-To-Data-1433 --priority 1010 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-asgs asg-app --destination-asgs asg-data \
  --destination-port-ranges 1433 --source-port-ranges '*'

Then the rule that does the actual segmentation work — an intra-VNet deny above the 65000 default and below your allows:

# Catch-all: anything VNet-internal not explicitly allowed above is denied
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Deny-All-VNet-Inbound --priority 4000 \
  --direction Inbound --access Deny --protocol '*' \
  --source-address-prefixes VirtualNetwork \
  --destination-address-prefixes VirtualNetwork \
  --destination-port-ranges '*' --source-port-ranges '*'

Now the evaluation order is: your allows (1000–1010) win for sanctioned flows, your deny (4000) blocks every other east-west attempt, and the permissive AllowVnetInBound at 65000 is never reached. That single deny rule converts “flat VNet” into “default-deny VNet.” You cannot delete or reorder the 65000 defaults — a lower priority number on your own rule is the only mechanism. Treat the 65500 DenyAllInBound as your safety net for non-VNet sources, and your 4000 Deny-All-VNet-Inbound as the one that disciplines lateral traffic.

The resulting evaluation walk, top to bottom, for an inbound packet:

Priority Rule Matches when… Outcome
200 Deny-Internet-SSH-RDP (platform) Internet → any on 22/3389 Deny (non-negotiable)
700 Allow-Admin-To-App-Mgmt admin subnets → asg-app mgmt ports Allow
1000 Allow-Web-To-App-443 asg-web → asg-app:443 Allow
1010 Allow-App-To-Data-1433 asg-app → asg-data:1433 Allow
4000 Deny-All-VNet-Inbound any other VNet→VNet Deny (the segmentation)
65000 AllowVnetInBound (default) never reached for VNet traffic (shadowed)
65500 DenyAllInBound (default) non-VNet, non-LB sources Deny (backstop)

Egress lockdown: the outbound half nobody writes

Most teams stop at inbound and leave AllowInternetOutBound (65001) wide open — which means a compromised host can exfiltrate freely. A mature baseline also disciplines outbound: allow the specific PaaS egress your workload needs (via service tags), allow east-west replies (stateful, automatic) and initiations you sanction, then deny the rest of the Internet.

# Allow data tier egress ONLY to Azure SQL + Storage in-region, deny the rest of Internet
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Allow-Data-To-AzureSQL --priority 1100 \
  --direction Outbound --access Allow --protocol Tcp \
  --source-asgs asg-data --destination-address-prefixes Sql.eastus2 \
  --destination-port-ranges 1433 --source-port-ranges '*'

az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Deny-All-Internet-Outbound --priority 4000 \
  --direction Outbound --access Deny --protocol '*' \
  --source-address-prefixes '*' --destination-address-prefixes Internet \
  --destination-port-ranges '*' --source-port-ranges '*'
Egress posture What it permits Risk it closes Cost of getting it wrong
Default (AllowInternetOutBound open) Any host → any Internet None Free data exfiltration path
Service-tag allow-list + Internet deny Only named PaaS/regions Exfiltration, C2 callbacks Break a legit egress → app fails
Force-tunnel to hub firewall (UDR) All egress via inspection Everything, with FQDN rules Firewall cost + latency + SPOF risk

The full baseline — inbound and outbound — reduces to six elements you can audit against: platform mandatory denies (100–499), platform mandatory allows for LB probes and management (500–899), the app team’s ASG-to-ASG allows (1000–3499), any app-specific denies (3500–3999), the east-west Deny-All-VNet-Inbound backstop (4000–4096), and the outbound Internet deny that sits after your PaaS egress allows. Miss the last two and the model is decorative.

Rule priority architecture: platform vs app-team

The moment more than one team writes into NSGs, priority numbers become a shared namespace — and an unmanaged shared namespace is a collision waiting to happen. Carve the 100–4096 range into bands with documented ownership:

Priority band Owner Purpose
100–499 Platform Mandatory denies (block RDP/SSH from Internet, known-bad ranges)
500–899 Platform Mandatory allows (management plane, AzureLoadBalancer health probes, monitoring egress)
900–999 Platform Reserved / future
1000–3499 App team Application allow flows between ASGs
3500–3999 App team App-specific denies
4000–4096 Platform The default-deny backstop (Deny-All-VNet-Inbound, Internet egress deny)

This is more than etiquette. Because first match wins, a platform deny at priority 200 will always beat an app-team allow at 1000 — exactly what you want for a non-negotiable control like “no inbound SSH from the internet, ever.” The platform’s mandatory rules live below the app team’s range, so app teams cannot punch a hole through them no matter what they write:

# Platform-mandated, low-number, app teams cannot override it
az network nsg rule create \
  --resource-group rg-platform --nsg-name "$NSG" \
  --name Deny-Internet-SSH-RDP --priority 200 \
  --direction Inbound --access Deny --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-address-prefixes '*' \
  --destination-port-ranges 22 3389 --source-port-ranges '*'

The banding gives you a contract: platform owns the floor and the ceiling, app teams own the middle. Encode the bands in your IaC module as input validation so a bad priority fails at plan time, not in production:

@description('App-team rule priority — must fall inside the app band')
@minValue(1000)
@maxValue(3999)
param rulePriority int

Enforcement is layered: the Bicep @minValue/@maxValue catches an out-of-band priority at what-if, PR review catches semantic mistakes (wrong ASG, too-wide a port), Azure Policy deny catches bad rule shapes (Internet mgmt ports, *-source allows) at resource write, and flow-log reconciliation catches dead or missing allows over time.

East-west isolation: blocking lateral movement in one subnet

Here is the payoff that subnet CIDR rules can never deliver. Put vm-app01 and vm-app02 in the same subnet, both labelled asg-app. With a plain subnet rule you cannot stop them from talking — they’re the same CIDR. With ASGs plus the default-deny baseline, app-to-app traffic is simply never on the allow list, so the 4000 deny catches it.

But sometimes peers legitimately need to talk (a clustered cache, a leader-election port) and you want to scope that, not open it wide. Express the intra-tier flow as ASG-to-the-same-ASG on exactly the port required:

# Allow app peers to gossip on 7000 ONLY; everything else app<->app stays denied
az network nsg rule create \
  --resource-group "$RG" --nsg-name "$NSG" \
  --name Allow-App-Peer-Gossip-7000 --priority 1200 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-asgs asg-app --destination-asgs asg-app \
  --destination-port-ranges 7000 --source-port-ranges '*'

Now vm-app01 can reach vm-app02 on 7000 and nothing else. If vm-app01 is compromised, the attacker cannot SSH to vm-app02, cannot hit its management agent, cannot scan its open ports — the baseline deny stands for every port except the one gossip flow. That is lateral-movement containment at the NIC, inside a shared subnet, which is the precise gap an IP-based NSG leaves open.

Intra-tier need How to express it What stays blocked
Cache/cluster gossip asg-app → asg-app on the single port SSH, RDP, agents, scans, other ports
Leader election asg-app → asg-app on the election port(s) Everything else east-west
No peer traffic at all Write nothing; the 4000 deny covers it All app-to-app traffic
Peer + health probe Gossip rule plus AzureLoadBalancer allow Non-probe, non-gossip flows

NSG placement: subnet vs NIC, and how they stack

You can attach an NSG to a subnet, to a NIC, or to both. Where you attach it decides the blast radius and how the rules combine.

When both exist, the two are evaluated in sequence and both must allow:

Direction Order of evaluation Result if either denies
Inbound to a NIC Subnet NSG (inbound) → NIC NSG (inbound) Packet dropped
Outbound from a NIC NIC NSG (outbound) → Subnet NSG (outbound) Packet dropped

This “both must allow” AND-logic is a frequent source of confusion: an allow on the NIC NSG does not override a deny on the subnet NSG. If your baseline deny lives on the subnet and you need one NIC to be an exception, you must place the allow at a lower priority than the deny on the same NSG that denies it — not on a different NSG expecting it to win.

Placement decision Choose subnet NSG Choose NIC NSG
Tier-wide baseline (deny-default)
Platform mandatory controls
One special host needs an extra port ✔ (or scope with an ASG on subnet NSG)
Appliance / NVA with unique needs
Keep the model auditable Prefer subnet + ASGs Avoid NIC NSG sprawl
Subnets that cannot carry an NSG (see next table)

Some subnets have special rules about NSGs. Know them before you design:

Subnet type NSG behaviour Design note
GatewaySubnet (VPN/ER gateway) NSG not recommended / limited Leave it to the platform
AzureFirewallSubnet NSG not supported Firewall governs itself
AzureBastionSubnet NSG supported but with required rules Follow the documented rule set
Regular workload subnet Full NSG support Your baseline goes here
Delegated subnet (e.g. App Service) Support varies by service Check the service’s requirements

Effective security rules: reading the merged truth

When a subnet NSG and a NIC NSG both apply, plus service tags and ASGs resolve to real prefixes, reasoning by hand gets error-prone fast. Effective security rules show the merged, resolved rule set actually in force on a NIC — defaults included, ASGs expanded, both NSGs combined. This is the source of truth.

# The effective (merged) inbound+outbound rules on a specific NIC
az network nic list-effective-nsg \
  --name nic-app-vm01 --resource-group "$RG" -o table

Pair it with IP Flow Verify, which answers “would this specific packet be allowed?” and names the deciding rule — without sending a packet:

az network watcher test-ip-flow \
  --resource-group "$RG" \
  --vm vm-app01 --nic nic-app-vm01 \
  --direction Outbound --protocol TCP \
  --local 10.0.2.4:50000 --remote 10.0.2.5:22
# Output: Access=Deny, RuleName=Deny-All-VNet-Inbound (or the relevant rule)
Diagnostic tool Question it answers When to reach for it
list-effective-nsg “What rules are actually in force on this NIC?” Two NSGs, ASGs, or tags in play
IP Flow Verify “Is this flow allowed, and which rule decided?” A specific connection is (or isn’t) working
Next Hop “Where does this packet route?” Suspect a UDR/firewall is intercepting
NSG diagnostic settings Rule counters over time Which rule is firing, how often
Flow logs Actual allow/deny per flow tuple Validation + audit evidence

If IP Flow Verify says Allow for a flow you intended to block, you have a higher-priority allow shadowing your deny — go re-read your priority bands. If it says Deny for a flow that should work, either your allow is missing or it’s numbered above a deny that fires first. See Why Is My Traffic Blocked? Debugging NSGs with Effective Rules and IP Flow Verify for the full playbook.

Private endpoints and NSG interaction

A private endpoint projects an Azure PaaS service (Storage, SQL, Key Vault) into your VNet as a private IP on a NIC. This is the correct way to reach PaaS without traversing the Internet or burning SNAT ports — but its interaction with NSGs has a history worth knowing.

Historically, NSG rules were not applied to traffic destined for a private endpoint; the platform bypassed them. Azure later added network policies for private endpoints — a subnet-level toggle (privateEndpointNetworkPolicies) that, when enabled, makes NSG (and UDR) rules apply to private-endpoint traffic. This changed the default posture over time, so the safe move is to set it explicitly rather than assume:

# Ensure NSG (and route) policies apply to PEs in this subnet
az network vnet subnet update \
  --resource-group "$RG" --vnet-name vnet-app --name snet-pe \
  --private-endpoint-network-policies Enabled
resource peSubnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
  parent: vnet
  name: 'snet-pe'
  properties: {
    addressPrefix: '10.0.9.0/27'
    privateEndpointNetworkPolicies: 'Enabled'   // NSG rules now govern PE traffic
  }
}

With policies enabled, you can write an NSG rule that permits only asg-app → private-endpoint-subnet:1433, so only the app tier reaches the SQL private endpoint — micro-segmentation extended to your PaaS data plane. The trade-offs and rules to remember:

Aspect Behaviour Design consequence
Default (policies disabled) NSG rules bypassed for PE traffic Anyone routable reaches the PE — no segmentation
Policies enabled NSG + UDR rules apply to PE Scope PE access by ASG/prefix
ASG on the PE itself Not the way you scope Scope by source ASG + PE subnet as destination
Egress to the PE Governed by source NIC’s outbound + PE subnet inbound Write both halves
Private DNS Separate concern (resolution) See the private-DNS article

For the DNS and at-scale side of private endpoints, see Private Endpoints and DNS at Scale: Centralized Private DNS Zone Architecture and the beginner-level Azure Private Endpoint vs Service Endpoint: Secure PaaS Access.

Azure Firewall vs NSG: when to layer

NSGs and Azure Firewall are not competitors — they operate at different altitudes and you use both. An NSG is a stateful L3/L4 packet filter at the subnet/NIC edge, free, and ideal for east-west micro-segmentation. Azure Firewall is a managed, centralised, stateful firewall in the hub that adds L7 features NSGs lack — FQDN filtering, application rules, threat intelligence, IDPS (Premium), and centralised logging — and it is where north-south and cross-VNet inspection belongs.

The decision is which flow, at which layer:

Dimension NSG Azure Firewall
Layer L3/L4 (five-tuple) L3/L4 + L7 (FQDN, app rules)
Placement Subnet / NIC (distributed) Hub subnet (centralised)
Best for East-west micro-segmentation North-south + cross-VNet inspection
FQDN / URL filtering No Yes (application rules)
Threat intel / IDPS No Yes (IDPS = Premium)
Identity/workload grouping ASGs IP groups / policy
Cost Free (pay for logs) Hourly + per-GB processed
Latency Negligible (inline at NIC) Extra hop through the hub
Single point of failure No (distributed) Yes if all egress force-tunnels through it
Logging Flow logs Rich firewall + IDPS logs

The layered pattern most enterprises land on: NSGs everywhere for east-west tier isolation (cheap, distributed, no latency), Azure Firewall in the hub for egress FQDN filtering and any cross-VNet flow needing L7 inspection (reached via UDR force-tunnelling from spokes), and private endpoints for Azure PaaS so that traffic never touches the firewall or the Internet at all.

Flow Right tool Why
web→app→data inside a VNet NSG + ASGs Distributed, free, identity-based
Two peered spokes talking Azure Firewall (hub) Cross-VNet; ASGs don’t span VNets
App → *.githubusercontent.com Azure Firewall (FQDN rule) NSGs can’t filter by FQDN
App → Azure Storage Private endpoint (+ NSG scope) No Internet, no SNAT, no firewall hop
Block Internet inbound mgmt NSG platform deny Cheapest, at the edge

For the firewall’s L7 story and the WAF that fronts web apps, see Application Gateway v2 WAF: End-to-End TLS, mTLS, and Custom Rule Tuning. For where the hub firewall physically sits, Hub-and-Spoke vs Virtual WAN: Choosing an Enterprise Cloud Network Topology.

Enforcing the model fleet-wide with Azure Policy

A baseline you apply by hand erodes within a quarter. Two enforcement jobs matter: every subnet must have an NSG, and no NSG may contain a rule that allows inbound SSH/RDP from the internet. Azure Policy does both — the first with a Modify/DeployIfNotExists effect, the second with Deny so the bad rule never lands.

There is a built-in for the dangerous-management-port case. Assign RDP access from the Internet should be blocked (and its SSH counterpart) to audit or deny. For a custom deny keyed on NSG rules, target the security-rule aliases:

{
  "policyRule": {
    "if": {
      "allOf": [
        { "field": "type", "equals": "Microsoft.Network/networkSecurityGroups/securityRules" },
        { "field": "Microsoft.Network/networkSecurityGroups/securityRules/access", "equals": "Allow" },
        { "field": "Microsoft.Network/networkSecurityGroups/securityRules/direction", "equals": "Inbound" },
        { "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", "in": ["Internet", "*", "0.0.0.0/0"] },
        { "anyOf": [
            { "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", "in": ["22", "3389", "*"] }
        ]}
      ]
    },
    "then": { "effect": "deny" }
  }
}

For “every subnet has an NSG,” use the built-in Subnets should be associated with a Network Security Group in audit mode first to size the gap, then escalate to a DeployIfNotExists that attaches a baseline NSG to any subnet missing one. The subnet alias to key off is Microsoft.Network/virtualNetworks/subnets[*].networkSecurityGroup.id.

Assign at the management-group scope so new subscriptions inherit it automatically:

az policy assignment create \
  --name deny-internet-mgmt-ports \
  --display-name "Deny inbound SSH/RDP from Internet" \
  --policy "<policy-definition-id>" \
  --scope "/providers/Microsoft.Management/managementGroups/mg-landingzones" \
  --enforcement-mode Default

To catch drift — someone editing an NSG out-of-band — lean on Policy’s continuous compliance scan (non-compliant resources surface in the compliance blade and via az policy state) and wire an Activity Log alert on Microsoft.Network/networkSecurityGroups/securityRules/write so an unexpected rule change pages someone in near real time.

The guardrails worth assigning, and what each buys:

Policy / control Effect Catches Scope
Subnets should have an NSG Audit → DeployIfNotExists Unprotected subnets Mgmt group
RDP from Internet should be blocked Audit / Deny Inbound 3389 from Internet Mgmt group
SSH from Internet should be blocked Audit / Deny Inbound 22 from Internet Mgmt group
Custom: no */Internet source on Allow to mgmt ports Deny Broad exposure rules Mgmt group
Custom: PE subnets have network policies enabled Audit / Deny PE traffic bypassing NSGs Mgmt group
Activity Log alert on securityRules/write Alert Out-of-band rule edits Subscription

For the mechanics of writing these, see Azure Policy Effects Explained: Deny, Audit, Modify, DeployIfNotExists.

Validating with flow logs and traffic analytics

A segmentation model you haven’t tested is a hypothesis. Validate it two ways: with flow telemetry and with a deliberate attempt to move laterally.

Flow logs. Enable NSG flow logs (or, going forward, VNet flow logs) with traffic analytics into Log Analytics. The deny you care about shows up as flow tuples with a deny decision. Query the analytics table for blocked east-west flows — this is your proof the baseline is firing, and your early warning for a missing allow:

AzureNetworkAnalytics_CL
| where SubType_s == "FlowLog" and FlowType_s == "IntraVNet"
| where FlowStatus_s == "D"            // D = Denied
| summarize DeniedFlows = count() by SrcIP_s, DestIP_s, DestPort_d, NSGRule_s
| order by DeniedFlows desc

A spike of denied intra-VNet flows on an unexpected port is either an attacker probing or a legitimate flow you forgot to allow. Both are exactly what you want surfaced. NSG flow logs and VNet flow logs differ — know which you’re deploying:

Aspect NSG flow logs VNet flow logs
Scope Per NSG Per VNet (or subnet/NIC)
Direction of travel Legacy path The recommended path forward
Captures Flows through NSG-attached resources Flows in the VNet regardless of NSG placement
Requires an NSG Yes No (broader coverage)
Traffic analytics Supported Supported
Storage Storage account + optional LA Storage account + optional LA

Lateral-movement test. From a host in asg-app, prove you cannot reach a peer on a non-sanctioned port, then confirm the sanctioned one works:

# From vm-app01, target vm-app02's private IP
nc -vz -w 3 10.0.2.5 22     # expect: timeout / connection refused (denied by 4000)
nc -vz -w 3 10.0.2.5 7000   # expect: succeeded (allowed by Allow-App-Peer-Gossip-7000)

Cross-check the decision without sending a packet using IP Flow Verify, which evaluates the effective NSG rules for a hypothetical flow and names the rule that decided it (shown earlier). A complete validation pass is four checks: the sanctioned flow succeeds (nc -vz <peer> 7000), the non-sanctioned one times out (nc -vz <peer> 22), IP Flow Verify names Deny-All-VNet-Inbound as the decider, and the deny tuples appear in the flow logs (FlowStatus_s == "D") — export the last as dated audit evidence. For a full detection pipeline built on this telemetry, see Network Flow Logs to Insight: Building a Traffic Analytics and Detection Pipeline.

Azure NSG/ASG vs AWS security groups and NACLs

If you carry an AWS mental model, the Azure constructs map cleanly — but with sharp differences that trip people. AWS splits the job across security groups (SGs) — stateful, attached to ENIs, allow-only, and referenceable by other SG IDs — and network ACLs (NACLs) — stateless, attached to subnets, with explicit allow and deny and numbered rules. Azure folds most of this into the NSG plus the ASG.

Concept Azure AWS
Stateful edge filter NSG (subnet or NIC) Security Group (ENI)
Stateless subnet filter (no direct equivalent) Network ACL (subnet)
Workload grouping ASG (label on NIC) SG referencing another SG by ID
Explicit deny rules NSG (Allow and Deny) Only NACL (SG is allow-only)
Rule ordering Priority number, first-match SG: no order (all evaluated); NACL: rule number
Return traffic Stateful — auto-allowed (NSG) SG stateful (auto); NACL stateless (needs rule)
Default posture AllowVnetInBound = flat east-west SG default-deny inbound; NACL default-allow
Multi-membership NIC in many ASGs ENI in many SGs
Cross-VNet/VPC ASG doesn’t span VNets; use tags/firewall SG reference doesn’t span VPCs; use peering/prefix lists
Named cloud endpoints Service tags Managed prefix lists / AWS service prefixes

The three differences that actually change design: on Azure you write explicit east-west denies because the NSG can deny, whereas an AWS SG is allow-only and relies on absence-of-allow plus a NACL; AWS gives you a coarse stateless subnet backstop (the NACL) that Azure has no direct equivalent for; and Azure is flat by default (AllowVnetInBound) where an AWS SG is default-deny inbound for free — so Azure needs the deliberate Deny-All-VNet-Inbound. The one-line translation: an ASG is Azure’s answer to “reference another security group as a source,” and that deny rule is how you recreate on Azure the default-deny posture AWS security groups give you out of the box. If you come from AWS, start at AWS VPC, Subnets and Security Groups Explained to anchor the SG/NACL model before mapping it across.

Architecture at a glance

Picture a single spoke VNet, 10.0.0.0/16, carrying a classic three-tier application, and follow a request left to right. Traffic from the Internet enters at an Application Gateway (with WAF) sitting in snet-agw (10.0.0.0/27). The gateway is the only tier with a public IP; everything behind it is private. The gateway forwards to the web tier — a VM Scale Set in snet-web (10.0.1.0/24), every instance born wearing the asg-web label because the ASG is set in the scale-set network profile. The web tier calls the app tier in snet-app (10.0.2.0/24, all NICs asg-app) on 443, and the app tier calls the data tier in snet-data (10.0.3.0/24, all NICs asg-data) on 1433. A separate snet-pe (10.0.9.0/27) holds the private endpoint for Azure SQL, reachable only by the app/data tiers.

Now overlay the policy. A subnet NSG on each workload subnet carries the same layered set: platform denies at 100–499 (no Internet SSH/RDP, ever), platform allows at 500–899 (the AzureLoadBalancer probe, monitoring egress), the app team’s ASG-to-ASG allows at 1000–1010 (asg-web→asg-app:443, asg-app→asg-data:1433), and — the keystone — the Deny-All-VNet-Inbound at 4000 that shadows the permissive AllowVnetInBound default at 65000. Because tiers are named by ASG not CIDR, two workloads in one subnet stay isolated and a scaled-out web instance is governed the instant it boots. East-west, only the three sanctioned flows exist; everything else — a compromised web box probing sideways, the app tier reaching another spoke, a stray service in a shared subnet reaching for the database — hits the 4000 deny and dies. North-south egress needing L7 (an app-tier call to a partner API by FQDN) force-tunnels via UDR to an Azure Firewall in the hub; egress to Azure SQL rides the private endpoint and never leaves the backbone. NSG flow logs stream every decision into Log Analytics, where the denied intra-VNet tuples are both audit evidence and early warning. You can trace one line — Internet → AppGw → asg-web → asg-app → asg-data → SQL PE — and read the whole security posture as a handful of sentences, which is exactly the point.

Real-world scenario

A fintech platform team ran a regulated workload where the audit requirement was explicit: the payments-processing tier must be reachable only from the API tier, on one port, and no other workload in the environment may initiate a connection to it. The catch was density. To control cost, payments pods and several unrelated internal services shared a single /23 subnet — a decision finance had already signed off on and would not reverse. Their existing NSG allowed 10.40.0.0/23 -> 10.40.0.0/23 on the payments port, which technically passed the port check but failed the isolation requirement: every service in that /23, payments or not, could open the payments port. The auditor flagged it as a finding.

They could not re-subnet without a migration window they didn’t have. The fix was to make identity, not address, the boundary. They labelled only the payments NICs asg-payments and only the API NICs asg-api, then replaced the CIDR rule with an ASG rule and let the default-deny baseline handle everyone else:

az network nsg rule create \
  --resource-group rg-pay-prod --nsg-name nsg-pay \
  --name Allow-Api-To-Payments-8443 --priority 1100 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-asgs asg-api --destination-asgs asg-payments \
  --destination-port-ranges 8443 --source-port-ranges '*'

With the Deny-All-VNet-Inbound backstop at priority 4000 already in place, the other services in the /23 — same subnet, same CIDR — lost all reachability to the payments tier the moment this rule shipped, because they were never granted an explicit allow. They proved it with IP Flow Verify from a non-API host (Access=Deny, RuleName=Deny-All-VNet-Inbound) and exported the flow-log denies as the audit evidence. The numbers that made it stick: 28 unrelated services in the /23 went from “can open 8443 to payments” to “cannot route to payments at all,” the payments tier’s east-west attack surface dropped from the whole subnet to one ASG on one port, and the auditor closed the finding in the same review cycle — total change two ASGs and one rule swap, zero migration. Six months later, when payments autoscaled from 12 to 40 pods, not a single NSG rule changed — every new pod inherited asg-payments and was governed at boot. Their one regret: not banding priorities from the start, which forced a careful renumber they’d have avoided by designing the bands up front.

Advantages and disadvantages

Advantages Disadvantages
Policy follows workload identity, not IP ASGs are intra-VNet only — cross-VNet needs tags/firewall
Same-subnet workloads fully isolated Requires deliberate design; not the default
Autoscale / re-image transparent (no rule edits) Priority-band discipline needed across teams
Rules read like sentences — auditable NSG is L3/L4 only — no FQDN/L7 (need firewall for that)
Default-deny east-west (with the 4000 rule) Easy to forget the outbound half (egress stays open)
Free (NSGs cost nothing; pay only for logs) Rule limits (1,000/NSG default; 10 ASGs/side) can bind
Distributed — no single point of failure Two-NSG stacking (subnet+NIC) confuses reasoning
Enforceable fleet-wide via Azure Policy Historical PE bypass surprises the unwary

When each side matters: the identity model and same-subnet isolation are decisive for regulated, dense, or autoscaling workloads — the exact cases subnet CIDRs fail. The disadvantages bite hardest when you need cross-VNet segmentation (reach for a hub firewall and IP groups instead of ASGs), L7 egress control (FQDN filtering is a firewall job), or when many teams share NSGs without a banding contract (collisions and accidental overrides). Micro-segmentation with NSGs/ASGs is the right default for east-west, intra-VNet isolation; layer a firewall for the rest.

Hands-on lab

Build a two-VM east-west isolation demo in one subnet, prove that ASGs isolate same-subnet peers, then tear it down. Free-tier-friendly (B1s VMs, deleted at the end). Run in Cloud Shell (Bash).

Step 1 — Variables and resource group.

RG=rg-microseg-lab
LOC=centralindia
VNET=vnet-lab
SUBNET=snet-workload
NSG=nsg-workload
az group create -n $RG -l $LOC -o table

Step 2 — VNet, subnet, and an NSG attached to the subnet.

az network vnet create -g $RG -n $VNET --address-prefix 10.10.0.0/16 \
  --subnet-name $SUBNET --subnet-prefix 10.10.1.0/24 -o table
az network nsg create -g $RG -n $NSG -o table
az network vnet subnet update -g $RG --vnet-name $VNET -n $SUBNET \
  --network-security-group $NSG -o table

Step 3 — Create the ASG and the default-deny east-west rule.

az network asg create -g $RG -n asg-app -l $LOC -o table

# The keystone: deny all VNet->VNet inbound above the 65000 default
az network nsg rule create -g $RG --nsg-name $NSG \
  --name Deny-All-VNet-Inbound --priority 4000 \
  --direction Inbound --access Deny --protocol '*' \
  --source-address-prefixes VirtualNetwork \
  --destination-address-prefixes VirtualNetwork \
  --destination-port-ranges '*' --source-port-ranges '*' -o table

Step 4 — Two VMs in the same subnet, both in asg-app. (No public IP; we’ll test peer-to-peer with Run Command / serial, so SSH from Internet is never opened.)

for n in vm-app01 vm-app02; do
  az vm create -g $RG -n $n --image Ubuntu2204 --size Standard_B1s \
    --vnet-name $VNET --subnet $SUBNET --public-ip-address "" \
    --admin-username azureuser --generate-ssh-keys --nsg "" -o none
  # Attach the VM's NIC ipconfig to asg-app
  NIC=$(az vm show -g $RG -n $n --query "networkProfile.networkInterfaces[0].id" -o tsv)
  az network nic ip-config update --ids "$NIC" --name ipconfig1 \
    --application-security-groups asg-app -o none
done
echo "Both VMs created and tagged asg-app."

Step 5 — Prove peer traffic is blocked (no allow exists yet). Grab the private IPs and test with IP Flow Verify — no packet needed:

IP2=$(az vm show -g $RG -n vm-app02 -d --query privateIps -o tsv)
NIC1=$(az vm show -g $RG -n vm-app01 --query "networkProfile.networkInterfaces[0].id" -o tsv)
az network watcher test-ip-flow -g $RG \
  --vm vm-app01 --nic "$NIC1" \
  --direction Outbound --protocol TCP \
  --local 10.10.1.4:50000 --remote ${IP2}:22
# Expect: Access=Deny, RuleName=Deny-All-VNet-Inbound

Expected: Access: Deny, RuleName: Deny-All-VNet-Inbound. Two peers in one subnet, both asg-app, cannot reach each other — the exact thing a CIDR rule cannot achieve.

Step 6 — Add a scoped intra-tier allow and re-test.

az network nsg rule create -g $RG --nsg-name $NSG \
  --name Allow-App-Peer-7000 --priority 1200 \
  --direction Inbound --access Allow --protocol Tcp \
  --source-asgs asg-app --destination-asgs asg-app \
  --destination-port-ranges 7000 --source-port-ranges '*' -o table

az network watcher test-ip-flow -g $RG \
  --vm vm-app01 --nic "$NIC1" \
  --direction Outbound --protocol TCP \
  --local 10.10.1.4:50000 --remote ${IP2}:7000
# Expect: Access=Allow, RuleName=Allow-App-Peer-7000

Expected: port 7000 now Access: Allow; re-run the port-22 test and it’s still Deny. You’ve scoped exactly one intra-tier flow and left everything else east-west denied.

Step 7 — Inspect the merged effective rules.

az network nic list-effective-nsg --ids "$NIC1" \
  --query "value[0].effectiveSecurityRules[?contains(name,'Allow-App-Peer') || contains(name,'Deny-All-VNet')]" -o table

Validation checklist. You attached an NSG to a subnet, created a default-deny east-west rule that shadows the 65000 default, tagged two same-subnet VMs into one ASG, proved with IP Flow Verify that they could not reach each other, then opened exactly one port between them and confirmed only that port works. No IPs in any rule; isolation by identity. Mapping steps to what each proves:

Step What you did What it proves
3 Deny-All-VNet-Inbound at 4000 The one rule that makes a VNet default-deny
4 Two VMs, same subnet, both asg-app The scenario CIDR rules cannot isolate
5 IP Flow Verify → Deny on 22 Peers isolated with zero allows
6 ASG→same-ASG allow on 7000 Scoped intra-tier flow, nothing wider
7 Effective rules The merged truth in force on the NIC

Cleanup.

az group delete -n $RG --yes --no-wait

Cost note. Two B1s VMs plus a VNet/NSG/ASG for an hour is well under ₹50; NSGs, ASGs, and IP Flow Verify are free. Deleting the resource group stops all charges.

Common mistakes & troubleshooting

The failure modes that bite in production — symptom, root cause, how to confirm, and the fix. Read the table at 02:14; the prose underneath expands the ones that hurt most.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 East-west traffic you expected to be blocked still flows No Deny-All-VNet-Inbound; the 65000 AllowVnetInBound is winning IP Flow Verify shows RuleName=AllowVnetInBound Add the 4000 deny above 65000
2 A sanctioned flow is blocked Allow numbered above a deny that fires first IP Flow Verify names the deny rule Renumber the allow below the deny
3 New autoscaled instance can’t be reached / can reach too much NIC not (or wrongly) in the ASG az network nic ip-config show --query applicationSecurityGroups Set ASG in VMSS network profile; roll instances
4 ASG rule rejected at create time Source & destination NICs in different VNets, or cross-region az network nsg rule create error; check VNet/region of NICs Use service tags/firewall for cross-VNet
5 “Cannot specify both ASGs and address prefixes” Mixed ASG + prefix in one field The create error itself Split into two rules
6 Health probes fail; LB marks backend down Denied AzureLoadBalancer inadvertently Effective rules; LB health blade Keep/add the AzureLoadBalancer allow
7 Private endpoint reachable by everyone PE subnet network policies disabled az network vnet subnet show --query privateEndpointNetworkPolicies Set to Enabled; add scoped NSG rule
8 NIC NSG “allow” doesn’t override subnet NSG deny Two NSGs AND-logic; both must allow list-effective-nsg shows subnet deny wins Put the allow below the deny on the same NSG
9 App-team rule silently ineffective Platform deny at lower priority shadows it IP Flow Verify names the platform deny Correct the flow at the platform, not with a higher app allow
10 Egress exfiltration path open despite “locked down” Only inbound was denied; AllowInternetOutBound still open Effective outbound rules show 65001 reachable Add outbound Internet deny after PaaS allows
11 Rule change didn’t drop an existing connection NSG statefulness — established flow persists Flow logs still show the flow Expect connection churn; force reconnect if needed
12 Baseline eroded over a quarter Out-of-band portal edits (drift) az policy state list; Activity Log on securityRules/write Enforce via Policy + alert; rules in IaC
13 az says priority already exists Duplicate priority in same direction az network nsg rule list --query "[].priority" Pick a free number (space rules 100 apart)
14 Service-tag egress broke after a while Used global tag; needed regional, or app talks to a new region az network list-service-tags; flow-log denies Use Sql.<region> etc.; add the needed region

1. East-west you meant to block still flows. Root cause: You added ASG allows but never added Deny-All-VNet-Inbound, so unsanctioned flows fall through to the permissive AllowVnetInBound at 65000. Confirm: IP Flow Verify on a flow you expected to block returns Access=Allow, RuleName=AllowVnetInBound. Fix: Add the intra-VNet deny at ~4000 (VirtualNetwork→VirtualNetwork, all ports). Allows shadow it; it shadows 65000.

7. Private endpoint open to everyone. Root cause: The PE subnet has privateEndpointNetworkPolicies disabled, so NSG rules are bypassed for PE traffic and anyone routable can reach it. Confirm: az network vnet subnet show --query privateEndpointNetworkPolicies returns Disabled. Fix: Set it to Enabled, then add an NSG rule permitting only the intended source ASG to the PE subnet on the service port.

8. NIC allow doesn’t beat subnet deny. Root cause: Subnet and NIC NSGs are AND-ed — both must allow. A permissive NIC NSG cannot override a deny on the subnet NSG. Confirm: list-effective-nsg shows the subnet’s deny as the deciding rule. Fix: Put the exception allow at a lower priority than the deny on the NSG that denies it (usually the subnet NSG), or scope the exception with an ASG so the subnet baseline permits it directly.

10. Egress still wide open. Root cause: You locked inbound and forgot outbound; AllowInternetOutBound at 65001 still permits any host to reach the Internet — a live exfiltration path. Confirm: Effective outbound rules show 65001 reachable; flow logs show outbound Internet flows succeeding. Fix: Allow the specific PaaS egress via service tags, then a Deny-All-Internet-Outbound at ~4000. Verify legitimate egress still passes before enforcing.

Best practices

Security notes

Threat Control in this article MITRE-ish mapping
Lateral movement inside a subnet ASG isolation + Deny-All-VNet-Inbound Lateral Movement
Internet-facing brute force on mgmt Platform deny (200) + Policy + Bastion Initial Access
Data exfiltration Egress allow-list + deny Internet out Exfiltration
Unmonitored drift → flat network Policy compliance + Activity Log alert Defense Evasion
Over-reachable PaaS data plane PE network policies + scoped NSG rule Collection / Lateral Movement

Cost & sizing

The good news: NSGs and ASGs are free. You do not pay per rule, per NSG, or per ASG. The cost of micro-segmentation is almost entirely in the adjacent services you layer and the telemetry you enable:

Cost driver Charge model Rough INR / month What it buys Watch-out
NSGs + ASGs Free ₹0 The entire segmentation model None
NSG/VNet flow logs (storage) Per-GB stored ~₹500–2,000 Deny evidence, validation Grows with traffic
Traffic analytics (Log Analytics) Per-GB ingested ~₹2,000–8,000 Queryable KQL, dashboards Sample chatty VNets
Azure Firewall Standard Hourly + per-GB ~₹35,000–45,000+ Cross-VNet + FQDN/L7 egress Only tunnel what needs L7
Azure Firewall Premium Hourly + per-GB Higher than Standard IDPS, TLS inspection Reserve for high-assurance
Private endpoint Hourly + per-GB ~₹800–1,500 each Scoped, backbone-only PaaS One per service per VNet
Azure Bastion (Basic) Hourly + per-GB ~₹12,000–15,000 No public mgmt ports Consider per-need start/stop

Sizing guidance: the segmentation itself scales to thousands of rules and ASGs at no cost — the default 1,000 rules per NSG (raisable to 5,000) and 10 ASGs per rule side are the practical ceilings, and augmented rules keep you well under them. Budget instead for flow-log ingestion (your biggest segmentation-adjacent bill) and, if you layer it, the firewall. A dense three-tier app with flow logs and a couple of private endpoints, no firewall, lands around ₹4,000–12,000/month in telemetry and PE charges; add a hub Azure Firewall and it jumps to ₹40,000+, which is why you push east-west to free NSGs and reserve the firewall for cross-VNet and FQDN egress only.

Interview & exam questions

1. Why can’t a subnet-CIDR NSG rule isolate two workloads in the same subnet, and what fixes it? A CIDR rule keys on an address range, and both workloads share that range — the rule cannot distinguish them, so any rule that permits “the subnet” permits both. The fix is Application Security Groups: label each workload’s NIC with a role (asg-a, asg-b) and write rules by ASG. Two NICs in one subnet then have different identities and can be isolated, because ASG membership, not address, is the boundary.

2. What is AllowVnetInBound and why does it matter for micro-segmentation? It’s a non-deletable default rule at priority 65000 that allows all VirtualNetwork → VirtualNetwork traffic — i.e., by default every VM in a VNet can reach every other on any port (a flat network). Micro-segmentation requires shadowing it with a lower-numbered explicit Deny-All-VNet-Inbound (e.g. 4000) so unsanctioned east-west traffic is denied before the 65000 allow is ever reached.

3. Explain NSG rule evaluation order. Rules are evaluated by priority, lowest number first, separately for inbound and outbound; the first rule that matches the packet’s five-tuple decides (Allow or Deny) and evaluation stops. This is why a platform Deny at 200 beats an app Allow at 1000, and why priority banding across teams matters.

4. When do you use a service tag versus an ASG? Use an ASG for intra-VNet, identity-based grouping of your own workloads (it’s an intra-VNet construct and can’t span VNets or regions). Use a service tag (Internet, Storage.<region>, Sql, AzureLoadBalancer, etc.) for Microsoft-managed prefix sets — cross-VNet, Internet, or Azure PaaS egress — where an ASG can’t reach and you don’t want to maintain CIDRs by hand.

5. What’s the difference between attaching an NSG to a subnet versus a NIC, and how do they combine? A subnet NSG governs every NIC in the subnet; a NIC NSG governs one NIC. When both apply, they’re AND-ed — for inbound, subnet-then-NIC, and both must allow or the packet drops; for outbound, NIC-then-subnet. A permissive NIC NSG cannot override a subnet NSG deny.

6. How are NSGs stateful, and how does that change your rules? NSGs track connections, so the return traffic of a permitted flow is automatically allowed — you don’t write a symmetric outbound rule for replies. You do still need an explicit outbound rule for flows your workload initiates. This contrasts with AWS NACLs, which are stateless and need rules for both directions.

7. A regulated tier must be reachable only from one upstream, but it shares a subnet with unrelated services. How do you enforce it without re-subnetting? Label the sensitive tier asg-target and the upstream asg-source, write a single asg-source → asg-target allow on the required port, and rely on the Deny-All-VNet-Inbound backstop to deny everyone else. The other same-subnet services, never granted an allow, lose all reachability — no migration needed. Prove it with IP Flow Verify and flow-log denies.

8. How do NSGs interact with private endpoints? Historically NSG rules were bypassed for private-endpoint traffic. Azure added the subnet toggle privateEndpointNetworkPolicies; when Enabled, NSG (and UDR) rules apply to PE traffic, letting you scope which source ASG may reach the PE. Set it explicitly rather than assume the default, then permit only the intended tier.

9. When do you reach for Azure Firewall instead of (or in addition to) NSGs? NSGs handle east-west L3/L4 micro-segmentation for free and distributed. Reach for Azure Firewall in the hub for cross-VNet inspection (ASGs don’t span VNets) and L7 controls NSGs lack — FQDN filtering, application rules, threat intel, IDPS. Layer both: NSGs everywhere, firewall for north-south and FQDN egress.

10. How do you keep a default-deny model from eroding across a large estate? Put NSG rules in IaC (Bicep/Terraform) with priority bands enforced as validation, deny out-of-band via least-privilege RBAC, and use Azure Policy at management-group scope (“subnets must have an NSG,” “deny Internet mgmt ports”) with an Activity Log alert on securityRules/write to catch drift in near real time. Continuous compliance scan surfaces anything that slipped.

11. What are the ASG limits that constrain a design? ASGs are intra-VNet and single-region (all NICs in a rule’s ASGs must share the VNet and region); a rule side accepts up to 10 ASGs; and you can’t mix ASGs and address prefixes in the same field of a rule. A NIC can belong to multiple ASGs. When you exceed these, regroup, split rules, or fall back to service tags / a hub firewall.

These map to AZ-700 (Designing and Implementing Azure Networking) — network security, NSGs, ASGs, service tags, Azure Firewall — and AZ-500 (Security Engineer) — network security groups, micro-segmentation, and Zero Trust network controls. A compact cert map:

Question theme Primary cert Objective area
NSG rules, priorities, defaults AZ-700 / AZ-500 Secure network connectivity
ASGs, service tags, augmented rules AZ-700 Design & implement network security
Subnet vs NIC NSG, effective rules AZ-700 Implement & manage NSGs
Private endpoints + NSG policy AZ-700 / AZ-500 Secure access to PaaS
Azure Firewall vs NSG layering AZ-700 Design & implement Azure Firewall
Policy guardrails, drift AZ-500 Manage security posture / governance

Quick check

  1. What single NSG rule converts a flat VNet into a default-deny one, and why does its priority matter relative to 65000?
  2. Two VMs share a subnet and both need to be reachable by the web tier but must not reach each other. Which construct makes this possible and how?
  3. True or false: attaching a permissive NSG to a NIC will override a deny on the subnet NSG.
  4. You locked all inbound east-west traffic but a compromised host can still exfiltrate to the Internet. What did you forget?
  5. Why can’t you use an ASG to segment traffic between two peered VNets, and what do you use instead?

Answers

  1. Deny-All-VNet-Inbound (VirtualNetwork → VirtualNetwork, all ports), placed at a low priority like 4000. Because NSGs evaluate lowest-number-first and stop at the first match, a rule numbered below the 65000 AllowVnetInBound default is reached before it — so unsanctioned east-west traffic is denied and the permissive default is never hit.
  2. Application Security Groups. Label both VMs asg-app and the web tier asg-web; write asg-web → asg-app allows for the sanctioned ingress, and rely on the default-deny baseline so asg-app → asg-app (peer traffic) is never allowed and the 4000 deny catches it. Identity, not CIDR, separates two NICs in the same subnet.
  3. False. Subnet and NIC NSGs are AND-ed — both must allow. A permissive NIC NSG cannot override a subnet NSG deny; put the exception below the deny on the NSG that denies it, or scope it with an ASG.
  4. The outbound half. Inbound denies don’t touch egress; AllowInternetOutBound at 65001 still permits any host to reach the Internet. Add PaaS egress allows via service tags, then a Deny-All-Internet-Outbound at ~4000.
  5. ASGs are an intra-VNet, single-region construct — all NICs referenced in a rule’s ASGs must share the VNet. Across peered VNets, use service tags or explicit prefixes for L3/L4, or route the cross-VNet flow through an Azure Firewall in the hub for inspection.

Glossary

Next steps

You can now design and validate identity-driven, default-deny micro-segmentation on Azure. Build outward:

AzureMicro-segmentationNSGASGZero TrustAzure FirewallFlow LogsSecurity
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading