DNS or Bust! Navigating the Wild West of Private Endpoints in Azure Virtual WAN
When setting up private networking in Azure, ensuring seamless name resolution for Point-to-Site (P2S) VPN clients accessing services via Private Endpoints can be tricky. One of the key challenges is properly configuring Azure DNS Resolver and ensuring that DNS Zones are centrally managed within a Virtual WAN (V-WAN) topology. The goal of this setup is to allow private name resolution across workloads without duplicating DNS zones and without requiring direct access to core networking resource groups.
This article walks through the setup of a V-WAN, a Private DNS Resolver, and the necessary configurations to link workloads to central Private DNS Zones using Terraform. This is a Terraform based implementation of the Virtual Hub Extension Pattern and employing the guidance of how to take advantage of the Azure Private DNS Resolver to allow VPN traffic to resolve the domain names of private-endpoint attached Azure Services.
One key learning is that DNS Zones should be centrally attached to a core virtual network shared across the V-WAN. Workloads need to reference these zones using a data source rather than creating new ones, ensuring a scalable and maintainable architecture. However, this also introduces an access control challenge, as workload teams might require permissions to create Virtual Network links in a core network they don’t own. We’ll also discuss potential role definitions to mitigate this issue.
By the end of this guide, you should have a solid understanding of how to configure private networking in Azure with Terraform, ensuring that P2S VPN clients can resolve Private Endpoint-attached services efficiently.
Setup the V-WAN
The first step in establishing private networking is setting up a Virtual WAN (V-WAN). This serves as the backbone of the private network, connecting all regional hubs and enabling seamless communication between workloads.
module "vwan" {
source = "Azure-Terraformer/vwan/azurerm"
version = "1.0.1"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
name = "${var.application_name}-${var.environment_name}"
primary_address_space = var.address_space
additional_regions = var.additional_regions
}
Setup the Private DNS Resolver
Next, we set up the Private DNS Resolver, which allows workloads and VPN clients to resolve private domain names. This is deployed in a virtual network linked to the V-WAN.
resource "azurerm_virtual_network" "dns" {
name = "vnet-${var.application_name}-dns-${var.environment_name}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
address_space = [var.dns_address_space]
}
resource "azurerm_virtual_hub_connection" "main" {
name = "dns-hub-connection"
virtual_hub_id = module.vwan.primary_virtual_hub_id
remote_virtual_network_id = azurerm_virtual_network.dns.id
}
It’s a good idea to attach the core Private DNS Zones to this central virtual network so that they are centrally managed and accessible to all workloads connected to the V-WAN.
resource "azurerm_private_dns_zone" "storage_blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_private_dns_zone_virtual_network_link" "storage_blob" {
name = "${var.application_name}-${var.environment_name}-storage-blob"
resource_group_name = azurerm_resource_group.main.name
private_dns_zone_name = azurerm_private_dns_zone.storage_blob.name
virtual_network_id = azurerm_virtual_network.dns.id
registration_enabled = false
}
Then I setup a pair of subnets on this network for the future home of my Azure Private DNS Resolver Network:
locals {
dns_resolver_inbound_subnet_address_space = cidrsubnet(var.dns_address_space, 4, 0)
dns_resolver_outbound_subnet_address_space = cidrsubnet(var.dns_address_space, 4, 1)
}
# minimum subnet size of /28
# https://learn.microsoft.com/en-us/azure/dns/dns-private-resolver-overview
resource "azurerm_subnet" "dns_resolver_inbound" {
name = "snet-dns-inbound"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.dns.name
address_prefixes = [local.dns_resolver_inbound_subnet_address_space]
delegation {
name = "Microsoft.Network.dnsResolvers"
service_delegation {
name = "Microsoft.Network/dnsResolvers"
actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
}
}
}
resource "azurerm_subnet" "dns_resolver_outbound" {
name = "snet-dns-outbound"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.dns.name
address_prefixes = [local.dns_resolver_outbound_subnet_address_space]
delegation {
name = "Microsoft.Network.dnsResolvers"
service_delegation {
actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
name = "Microsoft.Network/dnsResolvers"
}
}
}
Then I setup the Azure Private DNS Resolver with an Inbound and Outbound endpoint — both attached to their respective subnets.
resource "azurerm_private_dns_resolver" "main" {
name = "dnspr-${var.application_name}-${var.environment_name}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
virtual_network_id = azurerm_virtual_network.dns.id
}
resource "azurerm_private_dns_resolver_inbound_endpoint" "main" {
name = "${azurerm_private_dns_resolver.main.name}-inbound"
private_dns_resolver_id = azurerm_private_dns_resolver.main.id
location = azurerm_private_dns_resolver.main.location
ip_configurations {
private_ip_allocation_method = "Dynamic"
subnet_id = azurerm_subnet.dns_resolver_inbound.id
}
}
resource "azurerm_private_dns_resolver_outbound_endpoint" "main" {
name = "${azurerm_private_dns_resolver.main.name}-outbound"
private_dns_resolver_id = azurerm_private_dns_resolver.main.id
location = azurerm_private_dns_resolver.main.location
subnet_id = azurerm_subnet.dns_resolver_outbound.id
}
Setup the Point-to-Site (P2S) VPN
With the core network in place, we set up a P2S VPN to enable remote clients to securely connect to the private network:
locals {
dns_resolver_inbound_ip_address = azurerm_private_dns_resolver_inbound_endpoint.main.ip_configurations[0].private_ip_address
}
module "vpn" {
source = "Azure-Terraformer/vwan-vpn/azurerm"
version = "1.0.2"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
name = "${var.application_name}-${var.environment_name}"
virtual_hub_id = module.vwan.primary_virtual_hub_id
address_space = var.vpn_address_space
tenant_id = data.azuread_client_config.current.tenant_id
audience = local.azure_vpn_app_id
dns_servers = [local.dns_resolver_inbound_ip_address]
}
Setup a Storage Account with a Private Endpoint
Now that the network is in place, I can start attaching additional workloads to it. The Virtual WAN is the network. The Virtual Hubs that attach to the V-WAN extend this network to the regions that I want my workload(s) to be hosted within and the Virtual Networks of those workloads attach to the corresponding Virtual Hubs for the Azure regions they are hosted within.
As an Azure Terraformer, one of the first workloads I want to bring on is a Terraform State Backend in the form of an Azure Storage Account. Therefore, I need to provision one but I need to do it with a Private Endpoint to connect it to the network that I’ve established.
It needs to have its own Virtual Network that connects it to one of the Virtual Hubs:
resource "azurerm_virtual_network" "main" {
name = "vnet-${var.application_name}-${var.environment_name}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
address_space = [var.address_space]
}
data "azurerm_virtual_hub" "core" {
name = var.virtual_hub.name
resource_group_name = var.virtual_hub.resource_group
}
resource "azurerm_virtual_hub_connection" "main" {
name = "terraform-state-hub-connection"
virtual_hub_id = data.azurerm_virtual_hub.core.id
remote_virtual_network_id = azurerm_virtual_network.main.id
}
It needs to link to the central DNS Zones that I provisioned in my core-network environment.
data "azurerm_private_dns_zone" "storage_blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = var.virtual_hub.resource_group
}
resource "azurerm_private_dns_zone_virtual_network_link" "storage_blob" {
name = "${var.application_name}-${var.environment_name}-storage-blob"
resource_group_name = var.virtual_hub.resource_group
private_dns_zone_name = data.azurerm_private_dns_zone.storage_blob.name
virtual_network_id = azurerm_virtual_network.main.id
registration_enabled = false
}
Notice that I am using a Data Source to reference the privatelink.blob.core.windows.net DNS Zone and that I have to reference. an external Resource Group — the one for my core network. Unlike Bicep or ARM, Terraform’s ability to provision across Resource Group boundaries with ease makes these types of “attachable” infrastructure scenarios that much easier.
Lastly, I need a subnet and a Private Endpoint to attach the Storage Account to this workload network.
locals {
default_subnet_address_space = cidrsubnet(var.address_space, 2, 0)
}
resource "azurerm_subnet" "default" {
name = "snet-default"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = [local.default_subnet_address_space]
}
resource "azurerm_private_endpoint" "storage" {
name = "pe-${var.application_name}-${var.environment_name}-storage"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
subnet_id = azurerm_subnet.default.id
private_dns_zone_group {
name = "storage"
private_dns_zone_ids = [data.azurerm_private_dns_zone.storage_blob.id]
}
private_service_connection {
name = "pec-${var.application_name}-${var.environment_name}-storage"
private_connection_resource_id = azurerm_storage_account.main.id
is_manual_connection = false
subresource_names = ["blob"]
}
}
The Private endpoint links the Storage Account to the Subnet. The Subnet links us to the Virtual Network, the Virtual Network links us to the Virtual Hub, the Virtual Hub links us to the V-WAN which connects us to the Azure DNS Resolver, Point-to-Site (P2S) VPN and the rest of the broader network.
Connect to the VPN
I need to go download the VPN client configuration and import it into my Azure VPN client. Once, I’ve done that, I can connect and now my workstation is connected to the Azure V-WAN.
Now that I’ve setup my Blob Storage Account with a Private Endpoint I can verify my connectivity to it but simply doing an nslookup using its private link DNS fully qualified domain name (FQDN).
nslookup sttf8476dmzr.privatelink.blob.core.windows.net
Setup the DNS Resolver’s Inbound Endpoint’s Private IP Address as a DNS Server in your Azure VPN configuration.
It starts out like this:
<clientconfig i:nil="true" />
But you need to replace the
<clientconfig>
<dnsservers>
<dnsserver>10.47.3.4</dnsserver>
</dnsservers>
</clientconfig>
After you do that you should be able to resolve the Private Link DNS name!
Troubleshooting and Best Practices
- Create an Virtual Network for “Core” Network Infrastructure that follows the Single Responsability Principal (SRP). This is a great spot to put highly reusable stuff such as a DNS Zones, Private DNS Resolver, and Point-to-Site VPN Gateways.
- Using Data Sources for DNS Zones: Instead of creating new DNS Zones, reference the centrally managed ones using data.azurerm_private_dns_zone.
- Managing Access to Core Networking Resources: Workloads need permission to create Virtual Network links in the core network. Consider defining a custom role that allows only this specific action.
- Verifying Name Resolution: Ensure P2S VPN clients can resolve private domain names by checking their DNS configuration and performing nslookup tests.
Conclusion
By centralizing Private DNS Zones in a shared virtual network within the V-WAN, we create a scalable, maintainable and extensible networking architecture that supports multiple workloads without duplicating infrastructure. This approach ensures that as new workloads are deployed, they can seamlessly integrate with the existing network by referencing the core Private DNS Zones. Thus allowing us to easily attach Private Endpoints to the various Azure Services that are needed by our diverse set of workloads.
However, access management remains a challenge, and organizations should carefully design role-based access control to grant workload teams the necessary permissions without overexposing critical resources.
With these principles in mind, you can confidently set up private networking in Azure using Terraform, ensuring secure and efficient connectivity for all workloads all with those pesky public endpoints turned off for good!