Azure public IP addresses - keeping up with constant change

Public network access

Many Azure resources connect via public network - whether to public endpoints on other Azure resources, or to APIs and other public endpoints elsewhere on the internet.

Azure has many public IP addresses available for use by resources (workloads) running in Azure. Azure resources opening public network connections will use source IP addresses from that set. There are regional and resource type constraints: for example, an App Service in the East US region will use a source public IP address from the AppService.EastUS block. (We'll cover this in more detail below.)

When does all this matter to us? When we need to manage network access security for our own protected resources - that is, when we need to manage allow-lists (and perhaps also deny-lists) to an API or other resource(s) we manage. We may want to allow access from certain Azure public IP addresses to our resource(s), but block access from any other source, including all the other Azure public IP addresses.

Conceptually, this is simple. Let's get the outbound/source IP address(es) used by the resource we want to allow, and add those IP addresses to our allow-list. Network and security administrators are used to this task.

But it's not always that simple. What if the source IP address(es) change? What if they change often? Then we have to update our allow-lists and deny-lists just as often, or we may risk blocking out those who should be allowed, or allowing those who should be blocked.

Changes, changes...

Azure public IP addresses can change weekly! What does that mean for us?

Every week, there may be new IP addresses added to that list. IP addresses may also get removed if they will no longer be used by Azure.

Why does addition matter to us? Let's say some Azure resource (like an API or Function app) needs access to our resource. We previously added its public IPs - at that time - to our allow-list.

Now the set of Azure public IP addresses changes, and that Azure resource may use a new public IP address we did not know about! So even though we want to let that Azure resource in, it will be blocked - because its new source IP address, while legitimate, is not on our allow-list (yet).

Similarly, what if a public IP address Azure used previously gets removed, and Azure resources stop using it?

In this case, we will have an extra IP address on our allow-list. At least legitimate access will not be disrupted by this... but we will still have an IP address on our allow-list which may no longer belong to Azure, but may have been taken over by another party that we do not want to be able to access our resource.

Therefore, we also need to handle IP address removals, not just additions, so that we are not left with an ever-growing allow-list which includes IP addresses that may have changed ownership.

This is not a new issue in Azure - IP address and allow-list change management has been a network administration task as long as we have had networks and firewalls. The issue in Azure is that Azure public IP addresses could change as often as every week, and we have to keep up with those changes.

Alternatives to public IP addresses

Can we avoid using public IP addresses entirely? Yes, of course.

In Azure, we can use Private Link and Private Endpoints to keep our traffic completely on the Microsoft network, and use only private IP addresses. If your workloads are all in Azure and all support Azure private networking, then YES - that is a more secure networking approach than using public endpoints and public IP addresses.

We can also use Virtual Network Service Tags for supported Azure resource types instead of specifying IP addresses explicitly.

The context for this article is when you must use public IP addresses due to the details of your workload, which may include resources that only support public endpoints and IP addresses, or include non-Azure resources with which you need to communicate over the public internet.

Where can we get Azure public IP address ranges?

At this time, I know of three sources for Azure public IP address ranges.

  1. Azure IP Ranges and Service Tags – Public Cloud

  2. Azure Powershell: Get-AzNetworkServiceTag

  3. Azure REST API: REST Service Tags - List

Source 1 is updated weekly. The other two sources appear to be updated less frequently, and also use a different update numbering scheme than source 1.

In my research, this was also supported by discussions on GitHub issues like these:

For our purpose - to keep our allow-lists and deny-lists up to date as soon as possible after Azure public IP address change - source 1 seems to be the most reliable.

Therefore, in the rest of this article, I will focus on source 1.

Azure IP Ranges and Service Tags

This source of Azure public IP address ranges is a JSON file download. Unfortunately for automation scenarios, the download file URL changes each week, with the file name reflecting the date on which the file was updated. The web page also implements a redirect.

Note the details on that page, including the following important statements (emphasis added):

"This file is updated weekly. New ranges appearing in the file will not be used in Azure for at least one week. Please download the new json file every week and perform the necessary changes at your site to correctly identify services running in Azure."

This is why I said above "Azure public IP addresses can change weekly!".

The current version of this file at the time of this writing is ServiceTags_Public_20230904.json. After downloading it, we can open it in a good JSON editor, such as Visual Studio Code.

This file has 106,376 rows! This includes both IPv4 and IPv6 addresses for every public Azure region and many service tags. The section for AzureCloud.eastus, whose start is shown in the following block of JSON from the source file, contains almost 400 lines.

    {
      "name": "AzureCloud.eastus",
      "id": "AzureCloud.eastus",
      "properties": {
        "changeNumber": 97,
        "region": "eastus",
        "regionId": 32,
        "platform": "Azure",
        "systemService": "",
        "addressPrefixes": [
          "4.156.0.0/15",
          "4.227.128.0/17",
          "4.236.128.0/17",
...

The block I mentioned earlier in this article, AppService.EastUS, contains over 100 lines.

If we have more than a very small number of resources for which we need to manage allow-lists and deny-lists, doing so by parsing/searching through this file manually (every week!) very quickly becomes a significant administrative burden requiring time and risking errors.

Let's Automate!

Ideally, we could create some sort of automation which would do the following for us each week:

  • Get the latest Azure IP ranges and service tags file - that is, download it from Microsoft

  • Parse it and emit a structure with all the service tags and IP addresses, ideally allowing us to filter by service tag and for IPv4 only, if needed

We could use this structure in our own automations to manage our allow-lists. For example, let's say I have a resource of some kind, like an API to which I want to allow access to any Azure App Service running in East US.

I could download all current IPv4 addresses for AppService.EastUS, then I could iterate through that list and make sure each IPv4 address, or address range, is on my firewall's allow-list.

The part that will be the same in whatever such scenario we have is easily getting those Azure IP ranges each week, in a way that allows us to write automations and scripts specific to our situation. So whether I am updating a firewall's allow-list or an Azure Key Vault's network access rules, either way I first need that list of Azure IP ranges in a programmatic way.

I chose to implement this in Powershell, as Powershell is cross-platform (that would have sounded funny a few years ago...) and enables us to use either Azure Powershell or Azure CLI.

Please use modern cross-platform Powershell, not Windows Powershell.

The code is in my GitHub azure-deploy repo in /scripts/Network.ps1, which contains many networking utility methods. You can download and dot-source that code file; it has no other dependencies in that repo. Please make sure you have modern Powershell and Azure Powershell installed; this code file does not use the Azure CLI.

At a (modern, cross-platform) Powershell prompt in the directory where you downloaded Network.ps1, start by dot-sourcing the file so you can use its methods.

. ./Network.ps1

There are several relevant methods. Let's briefly look at each.

Get-AzurePublicIpRanges()

You can call this method as follows, storing its output in some local variable:

$azurePublicIpRanges = Get-AzurePublicIpRanges

This will take several seconds to run, since it's downloading the entire Azure public IP ranges JSON file and parsing it into a structure you can work with. You now have a local variable $azurePublicIpRanges. What does it contain?

This variable contains a structure that looks like the raw JSON file. It's an array of 2,450 objects:

$azurePublicIpRanges.GetType()

IsPublic IsSerial Name                                     BaseType    
-------- -------- ----                                     --------    
True     True     Object[]                                 System.Array

$azurePublicIpRanges.Count    
2450

Let's take a look at one of the objects:

$azurePublicIpRanges[0]

name        id          properties
----        --          ----------
ActionGroup ActionGroup @{changeNumber=30; region=; regionId=0; platform=Azure; systemService=ActionGroup; addressPrefixes=System.Object[]; networkFeatures=System.Object[]}

That's not too helpful. But remember - we have a structure in this array that resembles the raw JSON file. Let's explore more:

$azurePublicIpRanges[0].Properties

changeNumber    : 30
region          : 
regionId        : 0
platform        : Azure
systemService   : ActionGroup
addressPrefixes : {4.232.106.88/30, 13.66.143.220/30, 13.67.10.124/30, 13.69.109.132/30…}
networkFeatures : {API, NSG, UDR, FW}

That's beginning to look useful - the addressPrefixes property looks like an array of CIDRs (IP network specifications - SolarWinds has a good explanation). That's what we need! Let's get those to an array so we can iterate right through and do something with each CIDR:

$cidrs = $azurePublicIpRanges[0].Properties.AddressPrefixes
$cidrs.Count                                                  
143
foreach ($cidr in $cidrs) { Write-Debug -Message $cidr -Debug:$true }
DEBUG: 4.232.106.88/30
DEBUG: 13.66.143.220/30
DEBUG: 13.67.10.124/30
DEBUG: 13.69.109.132/30
... and so on

We now have a variable $cidrs, which is an array that we can iterate through and run some other script with each. That will vary by situation, but this is where you can substitute your own script and pass it the CIDR - for example, to add that CIDR to a firewall allow-list.

In the following example, implementation of the method that uses the CIDR - such as my example of Add-CidrToMyFirewall - is up to you.

foreach ($cidr in $cidrs) { Add-CidrToMyFirewall -Cidr $cidr }

Great! We now have a way of parsing and processing each weekly update of the Azure public IP ranges file.

But.... I left something out! Did you notice?

Above, I retrieve ALL public IP ranges (using Get-AzurePublicIpRanges), and then I just take the first one ($azurePublicIpRanges[0]) and work with it. What if that is not the one I need? What if I need to work with, say, the AppService.EastUS service tag and its IP addresses?

No problem! More code to the rescue.

Get-AzurePublicIpV4RangesForServiceTags()

I also wrote the Get-AzurePublicIpV4RangesForServiceTags() method. It's in the same file. As you might infer from the method name, it lets you filter to only the service tag(s) you need by taking a $ServiceTags parameter, which is a string array (so you can pass more than one).

Let's say I only want to work with Azure public IP addresses in AppService.EastUS and AppService.WestUS. Here's how to prepare my parameter, get the IP addresses, and again show the result is an array:

$serviceTags = @("AppService.EastUS", "AppService.WestUS")
$azurePublicIpRanges = Get-AzurePublicIpV4RangesForServiceTags -ServiceTags $serviceTags
$azurePublicIpRanges.GetType()

IsPublic IsSerial Name                                     BaseType    
-------- -------- ----                                     --------    
True     True     Object[]                                 System.Array

$azurePublicIpRanges.Count    
151

Great! So we have a much smaller array - remember, the first array of all Azure IP ranges above had a count of 2,450! This one only has 151. Let's explore some more like we did above.

$azurePublicIpRanges[0]
104.210.38.149/32

Wait a minute - when we did that with the output from Get-AzurePublicIpRanges(), there were Properties, and AddressPrefixes, and a whole structure. Here, the array members are just straight CIDRs!

Yes - this is intentional. I wrote this Get-AzurePublicIpV4RangesForServiceTags() to strip the result down to an array of only the CIDR strings - no added info. The reason was that if I am using this method - that is, already filtering to one or a few service tags - my most likely scenario is that I just want a list of CIDRs to process through for my real task (like managing firewall allow-list entries) and I don't need all the added information from the first method.

We can process straight through similarly to above:

foreach ($cidr in $azurePublicIpRanges) { Add-CidrToMyFirewall -Cidr $cidr }

See the difference? No need to traverse the child object hierarchy, just a straight list of CIDRs you can process through very quickly.

So there we are! Two Powershell methods which nicely wrap all the complexities of downloading and parsing the Azure public IP ranges file, and quickly give you an array you can just process through to do your actual allow-list maintenance work.

But wait! There's more!

Get-ServiceTagsForAzurePublicIp()

That method name - Get-ServiceTagsForAzurePublicIp() - looks backward! What's going on here?

Let's say you take over some resource that has an allow-list. It has lots (and lots, and lots...) of IP addresses already on the list. Or you are looking through an access log for some secured resource, and you see the source IP address for each access on each log record.

You need to go through those IP addresses - whether on your allow-list, or in your access log, or wherever... and you need to figure out what each of those IP addresses is! There are lots of network tools to do so (like ping -a), but what if you need to quickly figure out which Azure service tag an IP address belongs to?

Hmm - can't you just download that Azure public ranges raw JSON file and do a quick ctrl-F to see where your IP address is in that file?

Yes, if you're very lucky. But mostly, No. Why not?

Most of the entries in the Azure public IP ranges file are not individual IP addresses, they are ranges.

Let's say you find a source IP address of 20.119.13.183 in your list or log. You search for it in the Azure public IP ranges file... no match. So it's not an Azure public IP address then, right? Wrong - it still might be.

The Azure public IP ranges file mostly contains CIDRs with masks smaller than /32. A mask of /32 indicates an individual IP address, whereas a smaller mask like /24 or /20 indicates a range of IP addresses. (See the CIDR link above if you're not familiar with CIDR notation and masks.)

There are lots of entries in the Azure public IP ranges file, such as 20.119.0.0/20. So you look at that and wonder, hmm, does that contain my IP address 20.119.13.183?

If you're very practiced at networking... or if you have only a few of these to deal with... you can just eyeball this or use a (fantastic) tool like Solarwinds' Advanced Subnet Calculator to figure it out manually. But what if you have to do this weekly for thousands of IP addresses? That is, to figure out for each if it's an Azure public IP address or not, and if so, which service tag it belongs to (so you know which region and workload type it is)?

As usual, no problem, more code to the rescue.

What we need: a method to which we pass an IP address, and which tells us the Azure service tags (if any) that contains that IP address, even if the IP address is actually contained in a CIDR network specifier (that is, there's no exact text match).

How are Azure public IP addresses organized in the weekly file?

Can an Azure public IP address be contained in multiple service tags? That is, can it be specified several times in the file? Yes - because of how Azure public IP ranges are organized, a region's "AzureCloud.{region name}" service tag will contain all the IPs in that region, even if smaller service tags in the region also contain the same IP.

For example: the CIDR I mentioned above, 20.119.0.0/20, is actually (in the version of the Azure public IPs file I'm using to write this article) contained in two service tags: AppService.EastUS as well as AppService. That AppService service tag contains all the IPs for all regional AppService.{region name} service tags - this is why we need to understand how Azure public IPs are organized in this weekly file.

What does all this mean for us when we are trying to trace back a public IP from our log or list to Azure public IP ranges? It means that a given IP address or range may legitimately be contained in multiple Azure service tags. So we have to do two things:

  1. Get the service tag or tags which contain our IP address

  2. Decide which service tag to use for whatever other task we are doing

Let's look at an example with some more code (finally!). Back to the IP address I found in my log above, 20.119.13.183, which I already found has no text match in the Azure public IP ranges file. Let's see if we can find any Azure public IP range which contains this IP address:

$serviceTags = Get-ServiceTagsForAzurePublicIp -IpAddress "20.119.13.183"

$serviceTags.GetType()
IsPublic IsSerial Name                                     BaseType    
-------- -------- ----                                     --------    
True     True     Object[]                                 System.Array

$serviceTags.Count    
4

That looks like we found four (!) service tags which contain our IP address 20.119.13.183. Let's take a look at each of them:

foreach ($serviceTag in $serviceTags) { $serviceTag; "---" }

Name                           Value
----                           -----
Name                           AppService
Region                         (N/A)
Cidr                           20.119.0.0/20
---
Name                           AppService.EastUS
Region                         eastus
Cidr                           20.119.0.0/20
---
Name                           AzureCloud
Region                         (N/A)
Cidr                           20.119.0.0/17
---
Name                           AzureCloud.eastus
Region                         eastus
Cidr                           20.119.0.0/17
---

The first two results are what I expected: AppService and AppService.EastUS both have a 20.119.0.0/20 CIDR. We can enter this into Solarwinds' Advanced Subnet Calculator and indeed, confirm that this range's start and end IP address include our IP address.

But we have four results! What about the third and fourth?

AzureCloud and AzureCloud.eastus both have a 20.119.0.0/17 CIDR. We know this is a larger network than 20.119.0.0/20 (again, please consult the CIDR documentation linked above if you do not understand this notation). So what's going on here?

Again, we need to understand how Azure public IP ranges and service tags are organized. Let's spot-check these four service tags in the Azure public IP ranges file.

AzureCloud contains over 8,000 rows of CIDRs. Remember, each CIDR can itself be a network range and contain many IP addresses.

AzureCloud.eastus contains over 350 rows of CIDRs. Some spot-checking will confirm that, as we expect, all AzureCloud.eastus CIDRs are also listed in the overall AzureCloud CIDRs. This is why our IP address is found in both of those service tags above.

Similarly, AppService contains over 1,250 CIDRs, while AppService.EastUS contains 85. Spot-checking again, we confirm that, as expected, the AppService service tag contains all the AppService.EastUS CIDRs. This is why our IP address is found in both of those service tags.

Lastly, we can also see that AzureCloud contains AppService - because AzureCloud has the 20.119.0.0/17 CIDR, which contains the 20.119.0.0/20 CIDR.

This is why you may get multiple service tags which contain an IP address you are checking, such as in my example above. It is now up to YOU to decide, in your situation, which of those service tags you will use. Will you use the global one, or the regional one? Will you use AzureCloud or the more specific AppService? That depends on your specific scenario, but at least now you know exactly which Azure service tags contain your IP address!

Wrapping Up

In this article, I discussed Azure public IP address ranges, and how to easily keep up with weekly changes in a programmatic/automatable way.

You can use my Powershell script file to automate download/ingest of the Azure public IP ranges and adapt to your scenario. Here it is again:

https://github.com/plzm/azure-deploy/blob/main/scripts/Network.ps1

You can also trace back a given IP to determine if it is an Azure public IP address, and which service tag(s) contain it.

In an upcoming article, I'll show you a specific, non-trivial real-world scenario where the above knowledge and automation will be very helpful! Until then - happy IPing.