On 30 September 2025, default outbound access connectivity for virtual machines in Azure will be retired (see announcement). After this date, all new VMs that require internet access will need to use explicit outbound connectivity methods such as Azure NAT Gateway, Azure Load Balancer outbound rules, an Azure Firewall/NVA, or a directly attached Azure public IP address.
Existing VMs that use default outbound access will continue to work after this retirement, however, we strongly recommend transitioning to an explicit outbound method so that:
- Your workloads won’t be affected by public IP address changes.
- You have greater control over how your VMs connect to the internet.
- Your VMs use traceable IP resources that you own.
You can find a complete guidance on how default outbound access in Azure works and how to transition to an explicit outbound connectivity method in the VNet IP services documentation and also on this very detailed blog post. The options can be summarized as follows (any option works, but they are ordered by preference):
- Route the subnet Internet-bound traffic to an Azure Firewall or marketplace NVA, via a custom User-Defined Route. For enterprise-grade customers, this is the recommended approach, as it allows you to centralize egress and increase network security with features like packet filtering, FQDN-based rules, TLS inspection, or IDPS.
- Associate a NAT Gateway to the subnet of your virtual machines. Consider you need to create one NAT gateway per Virtual Network, because NAT Gateways cannot be reused across multiple Virtual Networks.
- Associate a Public Standard Load Balancer configured with outbound rules.
- Associate a Standard Public IP to any of the virtual machine’s network interfaces.
- For HTTP traffic only, forward outbound requests to a centralized HTTP proxy machine, provided this machine has one of the explicit outbound access methods above enabled.
Prepare your Azure Virtual Networks for this change soon enough, because after 30 September 2025, new virtual machines will not be able to connect directly to the Internet. If you have subnets in which all VMs are already using explicit outbound connectivity methods, it is advisable to make those subnets private, to avoid new implicit outbound cases before the deadline.
Now, you may ask “which of my Azure Virtual Machines or Scale Sets are using default outbound Internet access?” This question is helpful for characterizing your current compute estate and anticipate issues, but also to plan the transition to an explicit outbound method, for the reasons outlined above.
Luckily, there are some helpers. First, there is an Azure Advisor Operational Excellence recommendation (“Add explicit outbound method to disable default outbound”) that helps you pinpoint network interfaces that might be in this situation. However, this recommendation does not currently cover Virtual Machine Scale Sets or machines behind a Public Basic Load Balancer.
The second option is to use Azure Resource Graph and query the Azure control plane for resources in this situation. The two queries below address Virtual Machine and Virtual Machine Scale Set use cases. If there are resources with explicit outbound access via a Public Basic Load Balancer, those are also flagged, because Basic Load Balancers are also on the retirement path on the same date.
If you already have Azure Firewalls or NVAs to which you are routing egress traffic, don’t forget to replace the
resources
| where type =~ 'microsoft.network/virtualnetworks'
| mvexpand subnet = properties.subnets
| project
vnetName=tolower(name), resourceGroup, subscriptionId, location, subnetName = tolower(subnet.name), subnetId=tolower(subnet.id),
routeTable = tolower(subnet.properties.routeTable.id),
hasNatGW = iif(isnotempty(subnet.properties.natGateway.id), true, false)
| join kind=inner ( resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subscriptionName = name) on subscriptionId
| project-away subscriptionId, subscriptionId1
| order by subnetId
| join kind=leftouter ( resources | where type == 'microsoft.network/routetables' | project routeTable=tolower(id), routeTableProperties=properties ) on routeTable
| mv-expand route = routeTableProperties.routes
| project-away routeTable1, routeTableProperties
| extend routesToFW = iif(route.properties.addressPrefix == '0.0.0.0/0' and route.properties.nextHopType == 'VirtualAppliance' and route.properties.nextHopIpAddress in (''), true, false)
| extend routeName = tolower(route.name)
| project-away route
| join kind=leftouter (
resources
| where type == 'microsoft.network/networkinterfaces'
| extend vmId = tolower(properties.virtualMachine.id)
| where isnotempty(vmId)
| mv-expand ipConfiguration = properties.ipConfigurations
| mv-expand backendPool = ipConfiguration.properties.loadBalancerBackendAddressPools
| extend loadBalancerId = tolower(substring(backendPool.id, 0, indexof(backendPool.id, '/backendAddressPools')))
| join kind=leftouter (
resources
| where type == 'microsoft.network/loadbalancers'
| mv-expand frontEndIpConfiguration = properties.frontendIPConfigurations
| extend lbType = iif(isnotempty(frontEndIpConfiguration.properties.publicIPAddress.id), 'Public', 'Internal')
| mv-expand loadBalancingRule = properties.loadBalancingRules
| extend lbOutboundSnat = iif(lbType =='Public', iif(sku.name == 'Standard', not(tobool(loadBalancingRule.properties.disableOutboundSnat)), true), false)
| project loadBalancerId=tolower(id), lbSku=tostring(sku.name), lbType, lbOutboundSnat
| union (
resources
| where type == 'microsoft.network/loadbalancers'
| mv-expand frontEndIpConfiguration = properties.frontendIPConfigurations
| extend lbType = iif(isnotempty(frontEndIpConfiguration.properties.publicIPAddress.id), 'Public', 'Internal')
| mv-expand outboundRule = properties.outboundRules
| extend lbOutboundSnat = iif(isnotempty(outboundRule) or (sku.name == 'Basic' and lbType == 'Public'), true, false)
| project loadBalancerId=tolower(id), lbSku=tostring(sku.name), lbType, lbOutboundSnat
)
| summarize make_set(lbOutboundSnat) by loadBalancerId, lbSku, lbType
| extend lbOutboundSnat = iif(array_length(set_lbOutboundSnat) == 1, set_lbOutboundSnat[0], true)
) on loadBalancerId
| project nicId=tolower(id), vmId, subnetId=tolower(ipConfiguration.properties.subnet.id), hasPublicIP=iif(isnotempty(ipConfiguration.properties.publicIPAddress.id), true, false), lbOutboundSnat, lbSku
) on subnetId
| where isnotempty(vmId)
| extend lbOutboundSnat = iif(isnotempty(lbOutboundSnat), lbOutboundSnat, false)
| extend hasPublicIP=iif(isnotempty(hasPublicIP), hasPublicIP, false)
| summarize hasNatGW=make_set(hasNatGW), routesToFW=make_set(routesToFW), hasPublicIP=make_set(hasPublicIP), lbOutboundSnat=make_set(lbOutboundSnat), lbSku=make_set(lbSku) by vnetName, resourceGroup, subscriptionName, location, vmId
| extend hasNatGW = iif(array_length(hasNatGW) == 1, hasNatGW[0], true)
| extend routesToFW = iif(array_length(routesToFW) == 1, routesToFW[0], true)
| extend hasPublicIP = iif(array_length(hasPublicIP) == 1, hasPublicIP[0], true)
| extend lbOutboundSnat = iif(array_length(lbOutboundSnat) == 1, lbOutboundSnat[0], true)
| extend lbSku = tostring(lbSku[0])
| summarize by vmId, vmName=tostring(split(vmId,'/')[-1]), resourceGroup=tostring(split(vmId,'/')[4]), subscriptionName, location, vnetName, routesToFW, hasNatGW, lbOutboundSnat, lbSku, hasPublicIP
//| summarize vmCount=dcount(vmId) by routesToFW, hasNatGW, lbOutboundSnat, lbSku, hasPublicIP //uncomment this line and comment the last one to get only stats
| where not(hasNatGW) and not(routesToFW) and not(hasPublicIP) and (not(lbOutboundSnat) or lbSku == 'Basic') resources
| where type =~ 'microsoft.network/virtualnetworks'
| mvexpand subnet = properties.subnets
| project
vnetName=tolower(name), resourceGroup, subscriptionId, location, subnetName = tolower(subnet.name), subnetId=tolower(subnet.id),
routeTable = tolower(subnet.properties.routeTable.id),
hasNatGW = iif(isnotempty(subnet.properties.natGateway.id), true, false)
| join kind=inner ( resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subscriptionName = name) on subscriptionId
| project-away subscriptionId, subscriptionId1
| order by subnetId
| join kind=leftouter ( resources | where type == 'microsoft.network/routetables' | project routeTable=tolower(id), routeTableProperties=properties ) on routeTable
| mv-expand route = routeTableProperties.routes
| project-away routeTable1, routeTableProperties
| extend routesToFW = iif(route.properties.addressPrefix == '0.0.0.0/0' and route.properties.nextHopType == 'VirtualAppliance' and route.properties.nextHopIpAddress in (''), true, false)
| extend routeName = tolower(route.name)
| project-away route
| join kind=leftouter (
resources
| where type == 'microsoft.compute/virtualmachinescalesets' and properties.orchestrationMode == 'Uniform'
| mv-expand nicConfiguration = properties.virtualMachineProfile.networkProfile.networkInterfaceConfigurations
| mv-expand ipConfiguration = nicConfiguration.properties.ipConfigurations
| mv-expand backendPool = ipConfiguration.properties.loadBalancerBackendAddressPools
| extend loadBalancerId = tolower(substring(backendPool.id, 0, indexof(backendPool.id, '/backendAddressPools')))
| join kind=leftouter (
resources
| where type == 'microsoft.network/loadbalancers'
| mv-expand frontEndIpConfiguration = properties.frontendIPConfigurations
| extend lbType = iif(isnotempty(frontEndIpConfiguration.properties.publicIPAddress.id), 'Public', 'Internal')
| mv-expand loadBalancingRule = properties.loadBalancingRules
| extend lbOutboundSnat = iif(lbType =='Public', iif(sku.name == 'Standard', not(tobool(loadBalancingRule.properties.disableOutboundSnat)), true), false)
| project loadBalancerId=tolower(id), lbSku=tostring(sku.name), lbType, lbOutboundSnat
| union (
resources
| where type == 'microsoft.network/loadbalancers'
| mv-expand frontEndIpConfiguration = properties.frontendIPConfigurations
| extend lbType = iif(isnotempty(frontEndIpConfiguration.properties.publicIPAddress.id), 'Public', 'Internal')
| mv-expand outboundRule = properties.outboundRules
| extend lbOutboundSnat = iif(isnotempty(outboundRule) or (sku.name == 'Basic' and lbType == 'Public'), true, false)
| project loadBalancerId=tolower(id), lbSku=tostring(sku.name), lbType, lbOutboundSnat
)
| summarize make_set(lbOutboundSnat) by loadBalancerId, lbSku, lbType
| extend lbOutboundSnat = iif(array_length(set_lbOutboundSnat) == 1, set_lbOutboundSnat[0], true)
) on loadBalancerId
| project vmssId=tolower(id), subnetId=tolower(ipConfiguration.properties.subnet.id), hasPublicIP=iif(isnotempty(ipConfiguration.properties.publicIPAddressConfiguration), true, false), lbOutboundSnat, lbSku
) on subnetId
| where isnotempty(vmssId)
| extend lbOutboundSnat = iif(isnotempty(lbOutboundSnat), lbOutboundSnat, false)
| extend hasPublicIP=iif(isnotempty(hasPublicIP), hasPublicIP, false)
| summarize hasNatGW=make_set(hasNatGW), routesToFW=make_set(routesToFW), hasPublicIP=make_set(hasPublicIP), lbOutboundSnat=make_set(lbOutboundSnat), lbSku=make_set(lbSku) by vnetName, resourceGroup, subscriptionName, location, vmssId
| extend hasNatGW = iif(array_length(hasNatGW) == 1, hasNatGW[0], true)
| extend routesToFW = iif(array_length(routesToFW) == 1, routesToFW[0], true)
| extend hasPublicIP = iif(array_length(hasPublicIP) == 1, hasPublicIP[0], true)
| extend lbOutboundSnat = iif(array_length(lbOutboundSnat) == 1, lbOutboundSnat[0], true)
| extend lbSku = tostring(lbSku[0])
| summarize by vmssId, vmssName=tostring(split(vmssId,'/')[-1]), resourceGroup=tostring(split(vmssId,'/')[4]), subscriptionName, location, vnetName, routesToFW, hasNatGW, lbOutboundSnat, lbSku, hasPublicIP
//| summarize vmssCount=dcount(vmssId) by routesToFW, hasNatGW, lbOutboundSnat, lbSku, hasPublicIP //uncomment this line and comment the last one to get only stats
| where not(hasNatGW) and not(routesToFW) and not(hasPublicIP) and (not(lbOutboundSnat) or lbSku == 'Basic') Special thanks to my colleagues Pedro Pereira and Fernando Loureiro for having triggered the idea for this blog post.
