Blog Post

Core Infrastructure and Security Blog
6 MIN READ

How to identify Azure resources using default outbound Internet access?

hspinto's avatar
hspinto
Icon for Microsoft rankMicrosoft
Apr 07, 2025

The default outbound access problem

[UPDATE, May 28th: There will be no change to existing virtual networks. This means that there will be no change in the operation of existing or new virtual machines in these subnets.]

On 30 September 2025, default outbound access connectivity for virtual machines in Azure will be retired (see announcement). After this date, new virtual networks will default to using private subnets, meaning that an explicit outbound method must be enabled in order to reach public endpoints on the Internet and within Microsoft. These are the explicit outbound methods available: Azure NAT Gateway, Azure Load Balancer outbound rules, an Azure Firewall/NVA, or a directly attached Azure public IP address.

Existing VMs and virtual networks 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.

Explicit outbound solutions

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.

Identifying Azure resources using default outbound Internet access

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, 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 <firewall private IP> placeholders (line 14 of each query) by the private IP address of the firewall(s); otherwise, leave the query as is.

 

NOTE: the query below does not consider Azure Virtual WAN scenarios where Virtual Hubs have route tables forcing Internet-bound traffic to flow into an NVA, as Virtual Hubs route tables are not exposed yet in Azure Resource Graph.

Virtual Machines

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 ('<firewall private IP>'), 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')

Virtual Machine Scale Sets

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 ('<firewall private IP>'), 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')

Acknowledgments

Special thanks to my colleagues Pedro Pereira and Fernando Loureiro for having triggered the idea for this blog post.

Updated May 28, 2025
Version 4.0

2 Comments

  • VikasRay27's avatar
    VikasRay27
    Copper Contributor

    hspinto Great post, and the ARG queries are super helpful — thank you for sharing this! I had one question/suggestion though:

    In environments using Azure Virtual WAN or traditional hub-and-spoke topologies, we often push a 0.0.0.0/0 UDR to all spoke VNets to route internet-bound traffic through a centralized NVA or Azure Firewall in the hub. These routes appear in the effective routes of each VM’s NIC, so the traffic is explicitly routed — even though there may be no NAT Gateway, public IP, or outbound rules on the subnet.

    Just for clarity purpose, do you think it might be worth adding a small note or disclaimer that these queries may not fully apply in VWAN or hub-and-spoke setups where 0.0.0.0/0 routing is centrally managed and propagated — or even that this retirement scenario might not directly apply to such architectures, depending on how outbound traffic is controlled?

    • hspinto's avatar
      hspinto
      Icon for Microsoft rankMicrosoft

      Thank you for the feedback, VikasRay27. You are correct in which regards Azure Virtual WAN - this scenario was not included as it is not fully supported by Azure Resource Graph yet. I will add a note to make it clear.

      Regarding traditional hub & spoke approaches, the queries above support this scenario - you just have to replace the "<firewall private IP>" placeholder with the NVA's private IP address.