March 17

Azure Traffic Manager and its certificate (with Terraform)

tl;dr: Azure issues certificates using a 3rd-party service (currently Digicert). For the proof of possession of a managed domain, Azure gets a random string from Digicert and then exposes it under a known path. Digicert then fetches it, checks that it is correct and issues a cert. For App Services which have firewall open or have Digicert IPs whitelisted.


Azure Traffic Manager is one of the available in Azure load-balancing solutions. Its distinctive feature is that it works on DNS level, meaning that it kicks on only when customers are resolving host names of servers they want to connect to, but the connection itself is established directly to servers (using their IP addresses). There is no traffic 'flowing through' the Traffic Manager. The only traffic originating from the Traffic Manager is health probes – to determine if a server is still alive and capable of handling requests from real clients.

Traffic Manager works on DNS level. Although the DNS protocol belongs to OSI layer 7 (application), its results are passed down below to OSI layer 3 (network). Clients can connect to servers using any protocol, depending on which protocols the servers are listening to: FTP – if there is a file-server deployed, HTTP – if there is a web-server, and so on.

But typically, clients not only want to establish a connection – they also want the connection to be secure. Traffic Manager natively integrates with Azure App Services, which simplifies the setup. By default, the App Services come with a default domain protected with a wildcard certificate (fully managed by Azure which also takes over of TLS-termination, so that the code of the deployed application does not have to deal with it).

But Traffic Manager provides its own domain: {name}.trafficmanager.net. When trying to open its URL in a browser (using HTTPS), the browser immediately notices a mismatch.

Indeed, there is still a certificate only for the App Service's default domain. Let's then configure the HTTPS connectivity for the Traffic Manager domain too.

Securing domain in App Service

The domain of the Traffic Manager is considered pre-validated, thus, it appears automatically in the App Service's Custom Domains after we create an Endpoint in the Traffic Manager that targets the App Service.

To secure the custom domain, we need to (a) provision a certificate for it and (b) bind the certificate to the domain.

Step 1: provisioning a certificate.
Step 2: binding the certificate.

After ~5-10 min, the certificate can be bound to the custom domain. It is now possible to establish a proper secure connection to the App Service using the domain of the Traffic Manager.

Configured domain of Traffic Manager

App Service Firewall

Important: the steps above (precisely, step 1 - provisioning a certificate) work only if domain issuer can reach our App Service to validate possession of the domain we are requesting the certificate for.

Official documentation states that a certificate could be issued in that way only if an app is publicly accessible. This could become trouble for regulated businesses which require a firewall enabled in App Services.

A workaround for the trouble could be when we temporarily open up the App Services's firewall: either by completely disabling the firewall ("Enabled from all networks"), or by setting the "Unmatched rule action" to "Allow".

But, if the business is not only regulated but highly-regulated, then both of the options could be non-negotiable. For example, there could be an Azure Policy that automatically blocks provisioning of App Services with completely open firewall, and it is not possible to request temporary disabling of the policy or temporary exemption on the resource level. Still, there is a workaround for severe cases like that.

Digicert and proof-of-possession validation

When we keep the firewall of an App Service closed and try to provision a certificate, the process fails after 2–3 hours of waiting 😢.

Thank you Azure, that's very helpful...

Let's take a look once again at the picture above – there we can see that the issuer of the certificate of Traffic Manager is Digicert. This is also confirmed by the documentation: Azure has partnered with Digicert for issuance of all free certificates.

With trial and error, that's been found out that Azure uses HTTP practical demonstration for validation of domain possession. tl;dr: Digicert gives back to Azure a random string, and expects it to be exposed under http://{domain-to-be-validated}/.well-known/pki-validation/fileauth.txt. And indeed, we can see this random string available under this path (first, we need to whitelist our own IP in the App Service). The route is handled fully by the App Service, and opening it never reaches our code.

Ignore the warning next to https:// - the important part is that the challenge is accessible after whitelisting.

And it gives us Option 3. In the official documentation of Digicert we can then find the IPs that are used by the domain validation bots: 216.168.247.9 and 216.168.249.9. Once we whitelist the IPs, after a couple of minutes the domain is validated and the certificate gets successfully issued ✅.


Important: this approach has been found and verified in March 2025. The official documentation states the following:

Azure fully manages the certificates on your behalf, so any aspect of the managed certificate, including the root issuer, can change at anytime.

The approach of whitelisting Digicert IPs will work as long as (a) Digicert uses the same IPs and (b) Azure continues its partnership with Digicert. If any of the assumption is violated, the approach will break. Hopefully, it would be possible to find out the IPs of validation agents then too. In the worst case, there is always the option of temporarily complete opening of the firewall (explained above).


Automating with Terraform

All the steps explained above could be completely automated (so it would not be necessary to care about reproducing it in case of provisioning of a new resource group, a new App Service or a new Traffic Manager). The code below assumes that there is an azurerm_resource_group and an azurerm_linux_web_app available in the project.

Step 1: provisioning of Traffic Manager.

resource "random_uuid" "trafficManager" {} # For unique DNS name.

resource "azurerm_traffic_manager_profile" "trafficManager" {
  name                = "traffic-manager"
  resource_group_name = azurerm_resource_group.resourceGroup.name

  traffic_routing_method = "Weighted"

  dns_config {
    relative_name = "tm-${random_uuid.trafficManager.result}"
    ttl           = 30 # seconds
  }

  monitor_config {
    protocol = "HTTPS"
    port     = 443
    path     = "/ping"
  }
}

Step 2: provisioning of Traffic Manager Endpoint – which automatically creates a custom domain in the App Service.

Here we use a custom provisioner to add whitelisting of Digicert IPs.

data "azurerm_subscription" "subscription" {}

locals {
  ipsToWhitelist = ["216.168.247.9", "216.168.249.9"]
}

resource "azurerm_traffic_manager_azure_endpoint" "backend" {
  name               = azurerm_linux_web_app.appService.name
  profile_id         = azurerm_traffic_manager_profile.trafficManager.id
  target_resource_id = azurerm_linux_web_app.appService.id
  weight             = 1

  # Adding whitelisting - so it will be ready _before_ provisioning of certificate (below).
  provisioner "local-exec" {
    when        = create
    interpreter = ["pwsh", "-Command"]
    command     = <<-EOT
      $ipsToWhitelist = $env:ipsToWhitelist | ConvertFrom-Json
      foreach ($ip in $ipsToWhitelist) {
        az webapp config access-restriction add `
          --subscription $env:subscription `
          --resource-group $env:resourceGroup `
          --name $env:appService `
          --rule-name $ip `
          --priority 100 `
          --ip-address $ip `
          --action Allow
        if ($LASTEXITCODE -ne 0) {
          exit $LASTEXITCODE
        }
      }
    EOT
    environment = {
      subscription   = data.azurerm_subscription.subscription.subscription_id
      resourceGroup  = azurerm_linux_web_app.appService.resource_group_name
      appService     = azurerm_linux_web_app.appService.name
      ipsToWhitelist = jsonencode(local.ipsToWhitelist)
    }
    quiet = true
  }
}

Step 3: provisioning of certificate. This step takes ~10 min. After it is successfully completed, we also delete the whitelisting of IPs.

resource "azurerm_app_service_managed_certificate" "certificate" {
  depends_on = [azurerm_traffic_manager_azure_endpoint.backend]
  # Custom Hostname is created automatically when a Traffic Manager Endpoint is created.
  custom_hostname_binding_id = "${azurerm_linux_web_app.appService.id}/hostNameBindings/${azurerm_traffic_manager_profile.trafficManager.fqdn}"

  # After certificate is provisioned successfully - removing the IPs (which were provisioned above).
  provisioner "local-exec" {
    when        = create
    interpreter = ["pwsh", "-Command"]
    command     = <<-EOT
      $ipsToWhitelist = $env:ipsToWhitelist | ConvertFrom-Json
      foreach ($ip in $ipsToWhitelist) {
        az webapp config access-restriction remove `
          --subscription $env:subscription `
          --resource-group $env:resourceGroup `
          --name $env:appService `
          --rule-name $ip `
          --priority 100 `
          --ip-address $ip `
          --action Allow
        if ($LASTEXITCODE -ne 0) {
          exit $LASTEXITCODE
        }
      }
    EOT
    environment = {
      subscription   = data.azurerm_subscription.subscription.subscription_id
      resourceGroup  = azurerm_linux_web_app.appService.resource_group_name
      appService     = azurerm_linux_web_app.appService.name
      ipsToWhitelist = jsonencode(local.ipsToWhitelist)
    }
    quiet = true
  }
}

Step 4: binding the certificate to the domain.

resource "azurerm_app_service_certificate_binding" "bindingCertificateToDomain" {
  hostname_binding_id = "${azurerm_linux_web_app.appService.id}/hostNameBindings/${azurerm_traffic_manager_profile.trafficManager.fqdn}"
  certificate_id      = azurerm_app_service_managed_certificate.certificate.id
  ssl_state           = "SniEnabled"
}

The complete code is available on GitHub.