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