security logo

Using Azure Service Tags where they are not natively supported

Introduction

In this post, I will explain what service tags are, their purpose, and where they can be natively used in Azure. Additionally, I will provide overview on how to utilize service tags in places where they are currently not natively supported by using Terraform. Lastly, I will give an example of how service tags can be used in this context to allow access from Microsoft Power Platform in the UK to Azure KeyVault, Azure SQL, and CosmosDB.

What are service tags and what are they used for?

Service tags are a highly useful feature provided by Microsoft for managing network security in Azure. These tags consist of named groups of IP CIDRs, similar to IP Address groups found in traditional firewall solutions. Microsoft offers a comprehensive overview that you can find here.

To facilitate the integration of service tags into your network infrastructure, Microsoft also provides a convenient option to download the actual data in JSON format. This allows you to easily import the service tags into your firewall or security solution. You can access the JSON download here.

The use case for service tags is you can use them as a reference point in traffic filtering scenario’s. An example could be you have a microservice that uses an Azure SQL backend and Azure storage, and you want to harden the solution. You might want to limit outbound connectivity of the service so that if its compromised its harder for someone to exfiltrate data.

In this scenario you could limit outbound traffic using an NSG so only port 1433 for SQL, and 443 for HTTPs is allowed outbound. This would be a start in that any FTP or SFTP outbound traffic on standard ports would be blocked, but HTTPS could still easily be used to exfiltrate data. With service tags you can go one step further and say the destination for the 1433 rule needs to be to the service tag Sql so outbound connectivity is now limited to traffic destined for port 1433 and to SQL instances hosted in Azure. You can go even further as some service tags support regional granularity so you could use service tag Sql.EastUS so only Azure SQL instances in the EastUS region are an allowed destination. The same applies for the HTTPS destination, it could be narrowed down to Azure Storage only.

Its worth noting at this point, the service tags are not specific to your subscription or tenant. I.e. if you allow the service tag PowerPlatformInfra access to something, then anyone using Power Platform, in any subscription or tenant will be covered by this rule. Likewise if you allow access outbound to Sql.EastUS then any SQL instance in anyone’s subscription or tenant is covered by the rule.

Native support for service tags

Both Network Security Groups (NSGs) and Azure Firewall support service tags in their rule definitions.

You are out of luck if you are wanting to use service tags with public endpoint network restrictions for services like:

  • Storage Accounts
  • Azure SQL
  • Azure KeyVault
  • CosmosDB

It goes without saying, the best way to secure inbound traffic between others services and the above, is always going to be use private endpoint, and disable the public endpoint where this is possible.

In some cases depending on the services in question you may be able to make use of “trusted” services rules – there’s some further info here but the topic is outside the scope of this post – Configure Azure Storage firewalls and virtual networks | Microsoft Learn.

That said it isn’t always that simple. Lets say you have a Power Automate flow that you want to access KeyVault, SQL, or Cosmos. While there are service tags for Power Platform – as mentioned above there is no native support to use these on the network restrictions of the services above. The next section explains how terraform can be leveraged to bridge this gap.

How to use service tags where not natively supported

Terraform offers a data source for service tags. The documentation for the data source can be found here. This means terraform can easily bridge the gap.

The below example has two data sources used to get service tags for two different Power Platform regions. In the case of Power Platform, when you provision the environment you get asked which geographical region to provision it in, rather than regions in the context of Azure. In my example the Power Platform region was United Kingdom so the rule set will need to include all Azure regions in that geographical area.

HCL
data "azurerm_network_service_tags" "power_platform_uksouth" {
  service         = "PowerPlatformInfra"
  location        = "uksouth"
  location_filter = "uksouth"
}

data "azurerm_network_service_tags" "power_platform_ukwest" {
  service         = "PowerPlatformInfra"
  location        = "ukwest"
  location_filter = "ukwest"
}

Within the output given by the data source is list called ipv4_cidrs which is exactly what we will use. An example of the outputted object from the data source is below, I’ve removed the bulk of the CIDR’s for better readability.

HCL
{
  "address_prefixes" = tolist([
    "20.49.145.249/32",
    "2603:1061:2002:3000::/57",
  ])
  "id" = "uksouth-PowerPlatformInfra"
  "ipv4_cidrs" = tolist([
    "20.49.145.249/32",
    "20.49.166.40/32"
  ])
  "ipv6_cidrs" = tolist([
    "2603:1061:2002:3000::/57",
  ])
  "location" = "uksouth"
  "location_filter" = "uksouth"
  "name" = "PowerPlatformInfra.UKSouth"
  "service" = "PowerPlatformInfra"
  "timeouts" = null /* object */
}

KeyVault

In the case of KeyVault, like most other services it accepts a list IPs or CIDR’s so we can simply pass it the list. In my instance I want IP’s for both service tags so will use concat (documentation here) to combine the two lists.

HCL
resource "azurerm_key_vault" "example" {
  name                        = "examplekeyvault"
  location                    = azurerm_resource_group.example.location
  resource_group_name         = azurerm_resource_group.example.name
  enabled_for_disk_encryption = true
  tenant_id                   = data.azurerm_client_config.current.tenant_id
  soft_delete_retention_days  = 7
  purge_protection_enabled    = false

  sku_name = "standard"
  
  network_acls {
  # The Default Action to use when no rules match from ip_rules / 
  # virtual_network_subnet_ids. Possible values are Allow and Deny
  default_action = "Deny"
  
  # Allows all azure services to access your keyvault. Can be set to 'None'
  bypass         = "None"

  # The list of allowed ip addresses.
  ip_rules       = concat(
    data.azurerm_network_service_tags.power_platform_uksouth.ipv4_cidrs,
    data.azurerm_network_service_tags.power_platform_ukwest.ipv4_cidrs
    )
  }
}

CosmosDB

Cosmos is slightly different as it wants the list of IP’s and CIDRs in a coma separated list. The join function (documentation here) can be used to achieve this.

HCL
resource "azurerm_cosmosdb_account" "db" {
  name                = "tfex-cosmos-db-${random_integer.ri.result}"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  offer_type          = "Standard"
  kind                = "GlobalDocumentDB"
  enable_automatic_failover = true
  ip_range_filter = join(",",concat(
    data.azurerm_network_service_tags.power_platform_uksouth.ipv4_cidrs,
    data.azurerm_network_service_tags.power_platform_ukwest.ipv4_cidrs
    )
  )

  consistency_policy {
    consistency_level       = "BoundedStaleness"
    max_interval_in_seconds = 300
    max_staleness_prefix    = 100000
  }

  geo_location {
    location          = "uksouth"
    failover_priority = 0
  }
}

Azure SQL Server

For Azure SQL Server its slightly different again. Each rule needs:

  • A unique rule name
  • The starting IP of the range
  • The ending IP of the range

For singular IP’s the start and end of the range can be the same value. In this scenario service tags always give full CIDR annotation even for singular IP’s by using a suffix of /32. We can use the cidrhost function (documentation here) to get the start and end IP’s from a CIDR range.

We can use a local to get the data in a suitable format that we can loop over for each rule.

HCL
locals {
  sql_ip_rules = [for ip in concat(
    data.azurerm_network_service_tags.power_platform.uksouth.ipv4_cidrs,
    data.azurerm_network_service_tags.power_platform_ukwest.ipv4_cidrs
  ) : {
        rule_name        = "PowerPlatform-${cidrhost(ip, 0)}-${cidrhost(ip, -1)}"
        start_ip_address = cidrhost(ip, 0)
        end_ip_address   = cidrhost(ip, -1)
      }
  ]
}

resource "azurerm_mssql_server" "example" {
  name                         = "mssqlserver"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "missadministrator"
  administrator_login_password = "thisIsKat11"
  minimum_tls_version          = "1.2"

  azuread_administrator {
    login_username = "AzureAD Admin"
    object_id      = "00000000-0000-0000-0000-000000000000"
  }

  tags = {
    environment = "production"
  }
}

resource "azurerm_mssql_firewall_rule" "power_platform_firewall_rule" {
  for_each = { for rule in local.sql_ip_rules : rule.rule_name => rule }
 
  name             = each.value.rule_name
  server_id        = azurerm_mssql_server.example.id
  start_ip_address = each.value.start_ip_address
  end_ip_address   = each.value.end_ip_address
}

Closing thoughts

For the scenario’s where getting traffic to services is not possible privately, service tags offer great flexibility to add an additional layer of security vs just leaving a service with a public endpoint open to any network. Maybe native support to use service tags in other area’s will come with time. For now at least Terraform offers a viable solution.

Leave a Reply

Your email address will not be published. Required fields are marked *