Site icon Daniel's Tech Blog

Deploy Azure DNS security policies via Terraform

Today, I walk you through a new feature that has been released this year: Azure DNS security policies.

-> https://azure.microsoft.com/en-us/updates?WT.mc_id=AZ-MVP-5000119&id=497535

Azure DNS security policies allow you to get insights into your DNS traffic at the Virtual Network level. The two main use cases for DNS security policies are blocking name resolution of known or malicious domains and gaining detailed insights into your DNS traffic.

-> https://learn.microsoft.com/en-us/azure/dns/dns-security-policy?WT.mc_id=AZ-MVP-5000119
-> https://learn.microsoft.com/en-us/azure/dns/dns-traffic-log-how-to?WT.mc_id=AZ-MVP-5000119&tabs=sign-portal

Before we dive into the deployment of Azure DNS security policies via Terraform, let me highlight two important points.

First, DNS security policies are regional resources. Hence, you need at least one DNS security policy per region for your Virtual Networks. Second, luckily, DNS security policies have a 1:n relationship to Virtual Networks. That said, one single DNS security policy in a region can cover multiple Virtual Networks.

Now, let us dive into the Terraform configuration.

Terraform configuration

When we want to configure DNS security policies via Terraform, we must use the Terraform azapi provider. As of writing this blog post, the Terraform azurerm provider does not support DNS security policies yet.

A fully configured DNS security policy setup requires a total of four different resources. The first two resources, a DNS security policy and the domain list, can be configured independently of each other. The other two, the DNS security policy rules and the Virtual Network link configuration, depend on the other ones.

resource "azapi_resource" "dns_resolver_policy" {
  type      = "Microsoft.Network/dnsResolverPolicies@${local.api_version}"
  parent_id = var.resource_group_id
  name      = var.name
  location  = var.location
  tags      = var.tags
  body = {
    properties = {
    }
  }
}

Looking at the above Terraform code for a DNS security policy, we see that the policy does not have any special configurations.

resource "azapi_resource" "dns_resolver_domain_list" {
  for_each = var.domain_list

  type      = "Microsoft.Network/dnsResolverDomainLists@${local.api_version}"
  parent_id = var.resource_group_id
  name      = each.value.name
  location  = var.location
  tags      = var.tags
  body = {
    properties = {
      domains = each.value.domains
    }
  }
}

The domain list is a simple list of domains. We use a for_each directive to be able to create different domain lists for different DNS security policy rules.

resource "azapi_resource" "dns_resolver_policy_rule" {
  for_each   = var.rules
  depends_on = [azapi_resource.dns_resolver_domain_list]

  type      = "Microsoft.Network/dnsResolverPolicies/dnsSecurityRules@${local.api_version}"
  parent_id = azapi_resource.dns_resolver_policy.id
  name      = each.value.name
  location  = var.location
  tags      = var.tags
  body = {
    properties = {
      action = {
        actionType = each.value.action_type
      }
      dnsResolverDomainLists = local.dns_resolver_domain_lists[each.key]
      dnsSecurityRuleState   = each.value.dns_security_rule_state
      priority               = each.value.priority
    }
  }
}

Now it gets a bit more interesting. Again, we use for_each to be able to create several different rules. The special part about a rule is that it can be linked to 1 or n domain lists. Preparing the domain lists object is outsourced into a locals variable.

locals {
  api_version = "2025-05-01"
  dns_resolver_domain_lists = {
    for rule_key, rule in var.rules : rule_key => [
      for domain_list_key in rule.domain_list_keys : {
        id = azapi_resource.dns_resolver_domain_list[domain_list_key].id
      }
    ]
  }
}

The downside of that approach is that we must set the dependency between the DNS security policy rule and the DNS security policy explicitly. The implicit Terraform dependency graph is not working here.

resource "azapi_resource" "dns_resolver_vnet_link" {
  for_each = var.vnet_links

  type      = "Microsoft.Network/dnsResolverPolicies/virtualNetworkLinks@${local.api_version}"
  parent_id = azapi_resource.dns_resolver_policy.id
  name      = each.value.name
  location  = var.location
  tags      = var.tags
  body = {
    properties = {
      virtualNetwork = {
        id = data.azurerm_virtual_network.virtual_network[each.key].id
      }
    }
  }
}

Last but not least, we have to link the DNS security policy with a Virtual Network. Same as before, we use for_each. Out of convenience, we use a Terraform data source to retrieve the Virtual Network’s resource ID.

data "azurerm_virtual_network" "virtual_network" {
  for_each = var.vnet_links

  name                = each.value.name
  resource_group_name = each.value.resource_group_name
}

Now, we wire the things up and deploy the DNS security policy with all that we need.

locals {
  location                   = "northeurope"
  name                       = "dns-security-demo"
  log_analytics_workspace_id = "/subscriptions/<REDACTED>/resourceGroups/operations-management/providers/Microsoft.OperationalInsights/workspaces/azurekubernetesservice"
}

module "resource_group" {
  source = "../modules/resource_group"

  name     = local.name
  location = local.location
  tags = {
    environment = "demo"
    project     = "dns-security"
  }
}

module "dns_security_policy" {
  source = "../modules/dns_security_policy"

  name              = local.name
  resource_group_id = module.resource_group.id
  location          = local.location
  tags = {
    environment = "demo"
    project     = "dns-security"
  }
  domain_list = {
    domain_list_1 = {
      name    = local.name
      domains = ["danielstechblog.de."]
    }
  }
  rules = {
    rule_1 = {
      name                    = local.name
      action_type             = "Block"
      domain_list_keys        = ["domain_list_1"]
      dns_security_rule_state = "Enabled"
      priority                = 100
    }
  }
  vnet_links = {
    link_1 = {
      name                = "vnet-azst-1"
      resource_group_name = "rg-azst-1"
    }
  }
}

module "diagnostic_logs" {
  source = "../modules/diagnostic_logs"

  name                       = local.name
  backend_type               = "log_analytics"
  target_resource_id         = module.dns_security_policy.id
  log_analytics_workspace_id = local.log_analytics_workspace_id
  diagnostic_logs            = ["DnsResponse"]
}

As seen in the example above, we send diagnostic log data to an Azure Log Analytics workspace for convenient monitoring and analysis.

Testing

On our Azure Kubernetes Service cluster, we deploy a pod containing the tool dig to verify that the DNS security policy is working as intended to block the domain danielstechblog.de.

root@dnssecuritypolicy:/# dig +short www.danielstechblog.de
blockpolicy.azuredns.invalid.

root@dnssecuritypolicy:/# dig +short www.danielstechblog.io
217.160.0.92

Looking at the output, we get “blockpolicy.azuredns.invalid.” as a response to running dig +short www.danielstechblog.de. Whereas dig +short www.danielstechblog.io returns the corresponding IP for the DNS A record.

More insights are provided by the configured diagnostic logs.

Before rolling out and applying the DNS security policy to the Virtual Network, the DNS resolution worked for danielstechblog.de. Afterwards, it is blocked as intended.

Summary

When you have the requirement to centrally block DNS domains in a Virtual Network for all resources connected to it, like an Azure Kubernetes Service cluster, DNS security policies are the perfect fit and easy to set up.

If you have the requirement to block DNS domains for individual workloads and allow them for others within the same Virtual Network, then you might need another solution.

In a future blog post, we will look at Cilium and how to accomplish that on an Azure Kubernetes Service cluster.

You can find the Terraform DNS security policy module on my GitHub repository.

-> https://github.com/neumanndaniel/terraform/tree/master/modules/dns_security_policy

Exit mobile version