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:
- They cannot distinguish same-subnet workloads. An NSG rule keyed on a CIDR treats every NIC in that range identically. Put the payments API and the marketing site in one subnet to save on address space, and a rule that opens the database to “the subnet” opens it to both.
- They hardcode IPs into rules. Scale-out, re-imaging, and ephemeral DHCP churn the address space. Every churn either breaks a rule or tempts someone to widen it to a
/24“to be safe,” eroding the very boundary they meant to enforce. - They explode combinatorially. With N tiers across M spokes, explicit IP rules trend toward N×M and beyond. Nobody can read a 200-rule NSG and tell you what it permits — so nobody audits it, and dead rules accrete as permanent attack surface.
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:
- Read an NSG rule as a five-tuple, order rules correctly by priority, and predict exactly which rule a packet hits — including the non-deletable default rules at 65000/65001/65500.
- Use service tags (
VirtualNetwork,Internet,AzureLoadBalancer,Storage.<region>,Sql,AzureCloud, and the rest) and augmented security rules (multiple prefixes/ports/ASGs in one rule) to collapse rule sprawl. - Model workloads with Application Security Groups, attach NICs and VMSS instances to them, and respect the same-region / same-VNet constraints that bound the construct.
- Design a deny-by-default baseline that shadows the permissive
AllowVnetInBounddefault, and carve the 100–4096 priority range into platform vs app-team bands so mandatory controls can’t be overridden. - Decide NSG on subnet vs NIC, reason about how the two stack (both must allow, inbound and outbound evaluated separately), and read effective security rules to see the merged result.
- Explain how NSGs interact with private endpoints (and the historical vs current network-policy behaviour), and decide when to add Azure Firewall in the hub versus keep isolation at NSGs.
- Validate a segmentation model with NSG/VNet flow logs + traffic analytics and IP Flow Verify, and map the whole Azure model onto AWS security groups and NACLs.
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:
- Same region. An ASG and every NIC referencing it must live in the same Azure region.
- 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.
- Subnet NSG governs every NIC in the subnet — the coarse, tier-wide policy. This is where the deny-by-default baseline and platform mandatory rules usually live.
- NIC NSG governs one NIC — the fine, workload-specific exceptions. Use sparingly; too many NIC NSGs become impossible to reason about.
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
- Model ASGs on roles, not topology.
asg-web/asg-app/asg-dataplus cross-cutting labels (asg-patch-managed,asg-monitored). The name should describe what a workload is. - Always add
Deny-All-VNet-Inboundat ~4000. It’s the single rule that converts a flat VNet into default-deny by shadowing the 65000AllowVnetInBound. Without it, every allow is decoration. - Band your priorities and enforce them in IaC. Platform denies 100–499, platform allows 500–899, app flows 1000–3499, backstop 4000–4096. Encode the app band as
@minValue/@maxValueso a bad number fails atwhat-if. - Put platform mandatory controls at low numbers app teams can’t override. “No Internet SSH/RDP” at priority 200 beats any app allow at 1000, by construction.
- Don’t forget the outbound half. Lock egress to named PaaS via service tags, then deny the rest of the Internet. Inbound-only “lockdown” leaves an exfiltration path open.
- Prefer subnet NSGs + ASGs over NIC NSG sprawl. Two-NSG stacking (both must allow) is a reasoning tax; keep the baseline on the subnet and express exceptions with ASGs.
- Name rules like sentences.
Allow-Web-To-App-443is auditable at a glance;rule-7is not. The name is the only documentation that travels with the resource. - Use service tags instead of pasted CIDRs.
Storage.eastus2stays correct as Microsoft’s ranges change; a hardcoded block rots. Prefer the regional variant over the global tag. - Layer, don’t choose. NSGs for east-west; Azure Firewall in the hub for cross-VNet and FQDN/L7 egress; private endpoints (with
privateEndpointNetworkPoliciesenabled and scoped) for PaaS. Each tool at its altitude. - Enforce fleet-wide with Azure Policy and alert on drift. “Subnets must have an NSG” and “deny Internet mgmt ports” at management-group scope; Activity Log alert on
securityRules/write. - Reap dead rules. Any allow that flow logs show zero hits on over 90 days is a candidate for removal — a dead allow is standing attack surface.
Security notes
- Default-deny east-west is the control. Micro-segmentation’s security value is the
Deny-All-VNet-Inboundbackstop plus named allows — it directly shrinks lateral-movement (MITRE ATT&CK Lateral Movement) surface from “the whole subnet” to “one ASG on one port.” - Never expose management ports to the Internet. Inbound 22/3389 from
Internetis the most-attacked path in Azure. Deny it at a platform-owned low priority and enforce with Policy; reach hosts via Azure Bastion, not a public SSH/RDP rule. - Lock egress to close exfiltration. An attacker with a foothold needs an egress path. Allow only the PaaS/regions the workload needs (service tags), deny the rest of the Internet, and force any FQDN-based egress through a hub firewall with threat-intel filtering.
- Scope private endpoints. Enable PE network policies and permit only the app tier to the SQL/Storage/Key Vault private endpoint — the data plane deserves the same identity-based isolation as compute.
- Keep the model in code and reviewed. NSG rules in Bicep/Terraform, changed by PR. Portal edits are drift and drift is how a default-deny posture quietly becomes a flat one.
- Validate, don’t assume. A rule you didn’t test is a hypothesis. IP Flow Verify plus flow-log denies are both your proof and your audit evidence — export them for regulated workloads.
- Least privilege on who can edit NSGs. Scope
Network Contributor(or a tighter custom role) narrowly; a compromised operator identity that can rewrite NSGs can dismantle the whole model.
| 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:
- Flow logs cost the storage they land in plus, if you enable traffic analytics, Log Analytics ingestion (per GB) and the analytics processing. This is the main recurring cost of validating segmentation. Scope logging to the subnets that matter and sample sensibly on chatty environments.
- Azure Firewall (when you layer it for cross-VNet/L7) is the expensive piece — an hourly charge plus per-GB processed. Standard is cheaper than Premium (which adds IDPS/TLS inspection). Only force-tunnel through it what genuinely needs L7; keep east-west on free NSGs.
- Private endpoints carry a small hourly charge per endpoint plus per-GB processed — cheaper than the SNAT/firewall alternatives for PaaS, and they keep traffic off the Internet.
- Azure Bastion (the safe replacement for public SSH/RDP rules) is an hourly + per-GB charge; Basic is fine for admin access to a segmented estate.
| 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
- What single NSG rule converts a flat VNet into a default-deny one, and why does its priority matter relative to 65000?
- 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?
- True or false: attaching a permissive NSG to a NIC will override a deny on the subnet NSG.
- You locked all inbound east-west traffic but a compromised host can still exfiltrate to the Internet. What did you forget?
- Why can’t you use an ASG to segment traffic between two peered VNets, and what do you use instead?
Answers
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 65000AllowVnetInBounddefault is reached before it — so unsanctioned east-west traffic is denied and the permissive default is never hit.- Application Security Groups. Label both VMs
asg-appand the web tierasg-web; writeasg-web → asg-appallows for the sanctioned ingress, and rely on the default-deny baseline soasg-app → asg-app(peer traffic) is never allowed and the 4000 deny catches it. Identity, not CIDR, separates two NICs in the same subnet. - 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.
- The outbound half. Inbound denies don’t touch egress;
AllowInternetOutBoundat 65001 still permits any host to reach the Internet. Add PaaS egress allows via service tags, then aDeny-All-Internet-Outboundat ~4000. - 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
- Network Security Group (NSG) — a stateful L3/L4 packet filter (
Microsoft.Network/networkSecurityGroups) attached to a subnet or NIC, holding priority-ordered allow/deny security rules. - Security rule — one entry in an NSG: source, source port, destination, destination port, protocol, direction, access, priority.
- Priority — a number 100–4096; lower is evaluated first, first match wins and stops evaluation.
- Default rules — the six non-deletable rules at 65000/65001/65500, including the permissive
AllowVnetInBoundyou must shadow. AllowVnetInBound— the 65000 default that permits all VirtualNetwork→VirtualNetwork traffic — the reason a VNet is flat by default.- Application Security Group (ASG) — a role label (
asg-web) attached to a NIC’s IP config, usable as a rule source/destination; intra-VNet, single-region. - Service tag — a Microsoft-maintained named set of IP prefixes (
Internet,VirtualNetwork,Storage.<region>,Sql,AzureLoadBalancer, etc.). - Augmented security rule — a rule carrying multiple prefixes, ports, and/or ASGs in one entry (ASGs or prefixes per field, not both).
- Five-tuple — source, source port, destination, destination port, protocol — what a rule matches.
- Stateful — NSGs auto-allow the return traffic of a permitted flow; you don’t write a symmetric reply rule.
- Micro-segmentation — moving the unit of policy from an address range to a workload identity, yielding named, default-deny east-west flows.
- Deny-by-default / default-deny — a posture where nothing east-west is allowed except explicit named flows, achieved by shadowing the 65000 default with a
Deny-All-VNet-Inboundrule. - East-west traffic — traffic between workloads inside the VNet (as opposed to north-south, in/out of the VNet).
- Effective security rules — the merged, resolved rule set actually in force on a NIC (both NSGs, defaults, ASGs expanded), via
az network nic list-effective-nsg. - IP Flow Verify — a Network Watcher tool that says whether a hypothetical flow is allowed and names the deciding rule, without sending a packet.
- Private endpoint (PE) — a PaaS service projected into the VNet as a private IP; NSG rules apply only when
privateEndpointNetworkPoliciesis Enabled. privateEndpointNetworkPolicies— a subnet toggle that makes NSG/UDR rules govern private-endpoint traffic.- Azure Firewall — a managed, centralised, stateful L3/L4 + L7 firewall in the hub adding FQDN filtering, threat intel and IDPS that NSGs lack.
- Flow logs — NSG or VNet logs of allow/deny per flow tuple, optionally into traffic analytics, used to validate segmentation and provide audit evidence.
- Priority band — a documented range of priority numbers owned by a team (platform floor/ceiling, app-team middle) to prevent rule collisions.
- Network ACL (NACL) — the AWS stateless, subnet-level filter with explicit allow/deny; Azure has no direct equivalent (NSGs are stateful).
Next steps
You can now design and validate identity-driven, default-deny micro-segmentation on Azure. Build outward:
- Next: Application Security Groups (ASG): Tag-Based Microsegmentation Without IP Sprawl — go deeper on the ASG construct on its own.
- Related: Why Is My Traffic Blocked? Debugging NSGs with Effective Rules and IP Flow Verify — the troubleshooting companion when a flow is (or isn’t) working.
- Related: Private Endpoints and DNS at Scale: Centralized Private DNS Zone Architecture — extend segmentation to your PaaS data plane.
- Related: Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars — where network micro-segmentation fits in the larger strategy.
- Related: Network Flow Logs to Insight: Building a Traffic Analytics and Detection Pipeline — turn the deny telemetry into detections.
- Related: Hub-and-Spoke vs Virtual WAN: Choosing an Enterprise Cloud Network Topology — the topology that decides where a hub firewall inspects cross-VNet flows.