Private subnets by default: finding the Azure VMs still living on default outbound access
New Azure VNets now default to private subnets, but existing ones do not — here is how to find VMs relying on default outbound access and move them to explicit egress without an outage.
Every Azure estate I review has at least one of these: a VM with no public IP, no NAT gateway, no load balancer, and no route to a firewall — that still happily downloads packages from the internet. Nobody configured that path. It’s default outbound access, an implicit public IP owned by Microsoft that Azure hands out when you don’t define egress yourself.
That behavior is being wound down. For API versions released after 31 March 2026, subnets in new virtual networks are created with defaultOutboundAccess = false — they’re private subnets. The portal already did this. The part people miss is the other half of the sentence: existing virtual networks aren’t changed. So you now have two populations in the same tenant — new VNets that are private by default, and older ones that still quietly leak egress through IPs you don’t own.
This post is the runbook I use to find the second group and move it over cleanly.
Why this matters beyond “Microsoft said so”
The retirement is the trigger, but the real reasons are operational:
- The IP isn’t yours. The default outbound IP is Microsoft-owned and can change without notice. If a partner allowlists it, you have a latent outage.
- It’s not deterministic. Multiple NICs can produce inconsistent outbound IPs, and scale set instances don’t get consistent or contiguous addresses.
- It breaks things quietly. Default outbound IPs don’t support fragmented packets or ICMP.
- It contradicts Zero Trust. Internet egress that no one designed is internet egress no one monitors.
And the new-VNet default has its own trap: a team deploys a fresh spoke with a current Bicep API version, the VM boots, and Windows activation and Windows Update fail — because private subnets need an explicit outbound method for those too.
Step 1: find subnets that are still non-private
A subnet is private only when defaultOutboundAccess is explicitly false. A null value — which is what older API versions (and tools pinned to them, including Terraform providers that specify older versions) write — implicitly allows default outbound access. Azure Resource Graph gives you the tenant-wide view:
resources
| where type =~ 'microsoft.network/virtualnetworks'
| mv-expand subnet = properties.subnets
| extend doa = subnet.properties.defaultOutboundAccess
| where isnull(doa) or doa == true
| project subscriptionId, resourceGroup, vnet = name,
subnet = tostring(subnet.name),
natGateway = tostring(subnet.properties.natGateway.id),
routeTable = tostring(subnet.properties.routeTable.id)
| order by subscriptionId, vnet
Keep the NAT gateway and route table columns — they tell you which subnets already have an explicit path and are safe to flip first.
Step 2: find the VMs actually holding a default outbound IP
The subnet view tells you what’s possible; the NIC view tells you what’s happening. Each NIC carries a defaultOutboundConnectivityEnabled flag that tracks whether a default outbound IP was allocated. Azure Advisor surfaces the same thing under Operational Excellence as two separate recommendations — “Add explicit outbound method to disable default outbound” for VMs, and a second one for Virtual Machine Scale Sets. Check both.
For a scriptable list:
resources
| where type =~ 'microsoft.network/networkinterfaces'
| where properties.defaultOutboundConnectivityEnabled == true
| project subscriptionId, resourceGroup, nic = name,
vm = tostring(split(properties.virtualMachine.id, '/')[8])
One subtlety that generates a lot of confused tickets: a VM in a non-private subnet can still show a default outbound IP even when it already has a NAT gateway or a UDR to a firewall. That IP isn’t used for egress while the explicit method is in place, but the flag won’t clear until the subnet is private and the VM is stopped and deallocated.
Step 3: give every subnet an explicit egress path
Pick one method per workload, in rough order of preference:
- NAT gateway on the subnet — Microsoft’s recommended option for most scenarios, and the one I default to for spokes that need direct internet egress.
- UDR to Azure Firewall or an NVA — the hub-and-spoke norm when egress has to be inspected.
- Standard load balancer with outbound rules — when the VMs are already behind one.
- A Standard public IP on the NIC — only for the rare VM that genuinely needs to be internet-facing.
Two gotchas worth checking before you move on:
- UDRs with next hop
Internetbreak in private subnets. The classic pattern —0.0.0.0/0to the firewall, plus service tag routes with next hopInternetto bypass inspection — stops working for those bypass destinations unless there’s also an explicit outbound method (such as a NAT gateway) on the source subnet. Service endpoints are unaffected; they use a different next hop type. - Load balancer backend pools configured by IP address still use default outbound access due to a known issue. Put a NAT gateway on those subnets.
Also note that private subnets don’t apply to delegated or managed subnets hosting PaaS services — outbound there is the service’s responsibility.
Step 4: flip the subnet to private
Once egress is explicit, set the subnet to private. CLI, per subnet:
az network vnet subnet update \
--resource-group rg-spoke-prod \
--vnet-name vnet-spoke-prod \
--name snet-app \
--default-outbound false
Or PowerShell for every subnet in a VNet:
$vnet = Get-AzVirtualNetwork -ResourceGroupName 'rg-spoke-prod' -Name 'vnet-spoke-prod'
foreach ($subnet in $vnet.Subnets) {
if ($subnet.DefaultOutboundAccess -ne $false) {
$subnet.DefaultOutboundAccess = $false
Write-Output "Marking $($subnet.Name) as private"
}
}
Set-AzVirtualNetwork -VirtualNetwork $vnet
Then plan a maintenance window: existing VMs must be stopped and deallocated for the change to reach their NICs. A reboot isn’t enough. This is the step that turns a five-minute config change into a change-management ticket, so batch it with patching where you can.
Step 5: stop the drift in IaC
Don’t rely on API version defaults — make it explicit in every template so the intent survives provider pins and copy-paste:
resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = {
name: vnetName
location: location
properties: {
addressSpace: { addressPrefixes: [ '10.20.0.0/16' ] }
subnets: [
{
name: 'snet-app'
properties: {
addressPrefix: '10.20.1.0/24'
defaultOutboundAccess: false
natGateway: { id: natGateway.id }
}
}
]
}
}
Pair it with a policy that audits subnets where defaultOutboundAccess isn’t false, so the Resource Graph query in step 1 trends to zero instead of being a one-off cleanup.
For scale sets, flexible orchestration mode is already secure by default — instances don’t get a default outbound IP, so an explicit method is mandatory from day one.
The takeaway
The platform change only protects the VNets you haven’t built yet. The risk sits in the ones you already have: VMs with unowned, unmonitored egress that will keep working right up until an IP changes or someone flips a subnet without a NAT gateway behind it. Inventory with Resource Graph, add explicit egress first, flip to private second, deallocate third — and write defaultOutboundAccess: false into your modules so you never have to run this cleanup again.
Sources & further reading:
- Default outbound access in Azure — Microsoft Learn
- What is Azure NAT Gateway? — Microsoft Learn
- Outbound rules for Azure Load Balancer — Microsoft Learn
- Virtual network traffic routing — Microsoft Learn
- Default outbound access for VMs in Azure will be retired — Azure Updates