Azure DNS Private Resolver without VNet Peerings

As you might already know, Azure DNS Private Resolver is an Azure service that support DNS forwarding between Azure and on-premises DNS servers. It is very useful to provide Azure DNS resolution to on-premises clients (for example to access private endpoints), or to provide on-premises DNS resolution to Azure clients (to access on-prem resources).

Last week I needed to configure a lab to test a couple of questions in my head:

  • Do you need a VNet peering between the DNS client and the VNet containing the private resolver?
  • If not, do you need to link DNS private zones to the VNet?
  • And lastly, what does all this look like for P2S clients?

So I started furiously typing my keyboard (in this script you see every single command) to deploy this topology to Azure:

Here the main conclusions that I arrived to, and that we will explore in this post:

  • If using VNet links to forwarding rulesets, no IP connectivity between the client VNet and the private resolver VNet is required
  • If using VNet links to forwarding rulesets, linking private zones to the local VNet is actually required (linking the private zone to the hub is not enough)
  • For P2S VPNs you want to have the hub VNet using an IP address as DNS server, either a DNS proxy such as Azure Firewall, or the private resolver’s inbound endpoint IP address

By the way, in case you are wondering why I am using the Azure Firewall here, since it is not strictly required (you could use the private resolver’s inbound endpoint for the configuration in spoke 2), it is just because it is a very common scenario for customers to have a firewall in the hub, and have it configured as DNS proxy to enable FQDN-based network rules in the Azure Firewall policy.

Before moving on, if you don’t know what Azure DNS Private Resover is, you might want to check these other sources:

Using Forwarding Ruleset VNet links

Let’s double check that the VNet custom DNS server settings are configured as described in the diagram (not setting the dnsServer attribute for a VNet means using the default VNet Azure resolver):

❯ az network firewall show -n $azfw_name -g $rg -o tsv --query 'ipConfigurations[0].privateIpAddress'
192.168.1.4
❯ az network vnet show -n $vnet_name -g $rg --query 'dhcpOptions.dnsServers[0]' -o tsv
192.168.1.4
❯ az network vnet show -n $spoke1_vnet_name -g $rg --query 'dhcpOptions.dnsServers[0]' -o tsv

❯ az network vnet show -n $spoke2_vnet_name -g $rg --query 'dhcpOptions.dnsServers[0]' -o tsv
192.168.1.4

The previous output shows that both the hub and the spoke 2 VNets have the firewall configured as DNS server, but what does the firewall configuration look like? Let’s have a look:

❯ az network firewall policy show -n $azfw_policy_name -g $rg --query 'dnsSettings'
{
  "enableProxy": true,
  "requireProxyForNetworkRules": null,
  "servers": []
}

The previous output tells us that the Azure Firewall has DNS proxy functionality enabled, but it doesn’t have any DNS servers configured, meaning that it is going to use Azure’s native DNS resolver.

Now let’s make sure that we only have VNet peerings between the hub and spoke2, but not spoke1:

❯ az network vnet peering list -g $rg --vnet-name $vnet_name -o table
AllowForwardedTraffic    AllowGatewayTransit    AllowVirtualNetworkAccess    DoNotVerifyRemoteGateways    Name         PeeringState    PeeringSyncLevel    ProvisioningState    ResourceGroup    ResourceGuid                          UseRemoteGateways
-----------------------  ---------------------  ---------------------------  ---------------------------  -----------  --------------  ------------------  -------------------  ---------------  ------------------------------------  -------------------
True                     False                  True                         False                        hubtospoke2  Connected       FullyInSync         Succeeded            dns              efc6aed7-8b96-040a-3fd5-72eec18a7e4b  False
❯ az network vnet peering list -g $rg --vnet-name $spoke1_vnet_name -o table

❯ az network vnet peering list -g $rg --vnet-name $spoke2_vnet_name -o table
AllowForwardedTraffic    AllowGatewayTransit    AllowVirtualNetworkAccess    DoNotVerifyRemoteGateways    Name         PeeringState    PeeringSyncLevel    ProvisioningState    ResourceGroup    ResourceGuid                          UseRemoteGateways
-----------------------  ---------------------  ---------------------------  ---------------------------  -----------  --------------  ------------------  -------------------  ---------------  ------------------------------------  -------------------
True                     False                  True                         False                        spoke2tohub  Connected       FullyInSync         Succeeded            dns              efc6aed7-8b96-040a-3fd5-72eec18a7e4b  False

As additional verification, let’s check what the OS in the spoke VMs (Ubuntu) is using as DNS server. Our spoke 1 VM should be using Azure (168.63.129.16), and our spoke 2 VM Azure Firewall’s IP address (192.168.1.4):

❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke1_vm_pip" "netplan ip leases eth0 | grep DNS"
DNS=168.63.129.16
❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke2_vm_pip" "netplan ip leases eth0 | grep DNS"
DNS=192.168.1.4

One last thing: the forwarding ruleset that we are using in the private resolver should be associated to the hub and to spoke1, but we do not need it associated to spoke2:

❯ az dns-resolver vnet-link list --ruleset-name $fwd_ruleset_name -g $rg -o table
Name    ProvisioningState    ResourceGroup
------  -------------------  ---------------
hub     Succeeded            dns
spoke1  Succeeded            dns

Onprem DNS resolution

Finally, the moment of truth: resolving DNS names. We will use two test names:

  • test.contoso.com, defined in an onprem server. It should resolve to 1.2.3.4
  • test.contoso.corp, defined in an Azure DNS private zone. It should resolve to 5.6.7.8

Let’s start with spoke2, where we don’t expect any problem. This request will go through Azure Firewall (configured as DNS proxy) over the VNet peering, which will forward the request to the Azure resolver, which will forward the request to the private resolver (through the private link):

❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke2_vm_pip" "nslookup test.contoso.com"
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   test.contoso.com
Address: 1.2.3.4

For the VM in spoke 1 it will work as well, but this time instead the resolution going through the link between the forwarding ruleset and the spoke 2 VNet:

❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke1_vm_pip" "nslookup test.contoso.com"
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   test.contoso.com
Address: 1.2.3.4

DNS Private Zones

Before testing resolution of names defined in private zones, let’s have a quick look at the environment:

❯ az network private-dns zone list -g $rg -o table
ZoneName      ResourceGroup    RecordSets    MaxRecordSets    VirtualNetworkLinks    MaxVirtualNetworkLinks    VirtualNetworkLinksWithRegistration    MaxVirtualNetworkLinksWithRegistration    ProvisioningState
------------  ---------------  ------------  ---------------  ---------------------  ------------------------  -------------------------------------  ----------------------------------------  -------------------
contoso.corp  dns              2             25000            0                      1000                      0                                      100                                       Succeeded
❯ az network private-dns record-set a list -z $private_dns_zone -g $rg --query '[].{RecordName:name,IPv4Address:aRecords[0].ipv4Address}' -o table
RecordName    IPv4Address
------------  -------------
test          5.6.7.8
❯ az network private-dns link vnet list -g $rg -z $private_dns_zone -o table
LinkName    ResourceGroup    RegistrationEnabled    VirtualNetwork                                                                                                          LinkState    ProvisioningState
----------  ---------------  ---------------------  ----------------------------------------------------------------------------------------------------------------------  -----------  -------------------
hub         dns              False                  /subscriptions/e7da9914-9b05-4891-893c-546cb7b0422e/resourceGroups/dns/providers/Microsoft.Network/virtualNetworks/hub  Completed    Succeeded

We have the private zone contoso.corp, where we have an A record for test with the IP address 5.6.7.8, and we have the private zone linked only to the hub, but not to the spokes (essentially, the link shown in the picture at the beginning of this post between the DNS private zone and the spoke1 VNet is not there yet). Let’s see if this works for the spokes:

❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke1_vm_pip" "nslookup test.contoso.corp"
Server:         127.0.0.53
Address:        127.0.0.53#53

** server can't find test.contoso.corp: NXDOMAIN

❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke2_vm_pip" "nslookup test.contoso.corp"
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   test.contoso.corp
Address: 5.6.7.8

Since the VM in spoke 2 is using the Azure Firewall as custom DNS server, which is deployed in the hub (where the private zone is linked), spoke2 VMs can successfully resolve names in private DNS zones. However, the case of the VM in spoke1 is interesting: it can leverage the forwarding rules in the private resolver, but it cannot leverage the private resolver to access private zones linked to the hub VNet. To make this work, we need to link the private zone to spoke1 as well:

❯ az network private-dns link vnet create -g $rg -z $private_dns_zone -n $spoke1_vnet_name --virtual-network $spoke1_vnet_name --registration-enabled false -o none
❯ ssh -n -o BatchMode=yes -o StrictHostKeyChecking=no "$spoke1_vm_pip" "nslookup test.contoso.corp"
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   test.contoso.corp
Address: 5.6.7.8

Private Resolver and Point-to-Site VPNs

The key to realize here is that an Azure P2S client will get assigned as DNS server whatever the VNet containing the VPN gateway has configured as custom DNS server. Since the hub VNet (where the VPN gateway is) has as custom DNS the Azure Firewall’s IP address (192.168.1.4), that is the DNS server that a VPN client should receive:

In the previous screenshot, note how the DNS server is set to 192.168.1.4. And sure enough, our system can get the IP addresses for the hostnames we are testing. First of all, let’s verify connectivity to our hub VNet, where we have a test VM deployed with the IP address 192.168.10.4:

❯ traceroute 192.168.10.4
traceroute to 192.168.10.4 (192.168.10.4), 30 hops max, 60 byte packets
 1  172.22.192.1 (172.22.192.1)  0.737 ms  0.644 ms  0.606 ms
 2  clientvm.internal.cloudapp.net (192.168.10.4)  27.315 ms  30.103 ms  29.735 ms

This is running on my WSL, so we are going through the Windows OS (172.22.192.1), and then through the VPN Gateway (which, fun fact, is transparent to traceroute). Now let’s test domain resolution:

❯ nslookup test.contoso.com
Server:         172.22.192.1
Address:        172.22.192.1#53

Non-authoritative answer:
Name:   test.contoso.com
Address: 1.2.3.4

❯ nslookup test.contoso.corp
Server:         172.22.192.1
Address:        172.22.192.1#53

Non-authoritative answer:
Name:   test.contoso.corp
Address: 5.6.7.8

Adding up

Et voilà! This was the main section of the post. If you are wondering which of the two scenarios you should use, you know what’s coming: it depends. If you have an Azure Firewall, you probably want to use it as DNS proxy to have access to FQDN-based network rules, so I think that is going to be the most common design. However, there might be certain scenarios where you want to leverage the design without VNet peerings, for example in situations where you have some IP overlap.

Following you can find some useful Azure CLI commands for the Azure DNS private resolver.

Reference: Creating an Azure DNS Private Resolver with Azure CLI

You can find the full list of commands in the Azure CLI script in Github, here an extract of the relevant commands that create the private resolver, the endpoints and the ruleset:

in_ep_subnet_id=$(az network vnet subnet show --vnet-name $vnet_name -n $resolver_in_subnet_name -g $rg --query id -o tsv)
in_ep_config="[{private-ip-address:'',private-ip-allocation-method:'Dynamic',id:'${in_ep_subnet_id}'}]"
out_ep_subnet_id=$(az network vnet subnet show --vnet-name $vnet_name -n $resolver_out_subnet_name -g $rg --query id -o tsv)
out_ep_config="[{private-ip-address:'',private-ip-allocation-method:'Dynamic',id:'${out_ep_subnet_id}'}]"
echo "Creating DNS Private Resolver..."
vnet_id=$(az network vnet show -n $vnet_name -g $rg --query id -o tsv)
az dns-resolver create -n $resolver_name  --id $vnet_id -g $rg -o none
az dns-resolver inbound-endpoint create --dns-resolver-name $resolver_name --name $in_ep_name \
        --ip-configurations $in_ep_config -g $rg -o none
az dns-resolver outbound-endpoint create --dns-resolver-name $resolver_name --name $out_ep_name \
        --id $out_ep_subnet_id -g $rg -o none
in_ep_id=$(az dns-resolver inbound-endpoint show --dns-resolver-name $resolver_name --name $in_ep_name -g $rg --query id -o tsv)
out_ep_id=$(az dns-resolver outbound-endpoint show --dns-resolver-name $resolver_name --name $out_ep_name -g $rg --query id -o tsv)
az dns-resolver forwarding-ruleset create -n $fwd_ruleset_name -g $rg \
        --outbound-endpoints "[{'id': '$out_ep_id'}]"
az dns-resolver forwarding-rule create --ruleset-name $fwd_ruleset_name -n $fwd_rule_name \
        --domain-name "${sample_domain}." --forwarding-rule-state "Enabled" \
        --target-dns-servers "[{ip-address:'$dns_vm_privateip',port:53}]" -g $rg -o none
spoke1_vnet_id=$(az network vnet show -n $spoke1_vnet_name -g $rg --query id -o tsv)
az dns-resolver vnet-link create --ruleset-name $fwd_ruleset_name \
        --id "$spoke1_vnet_id" -g $rg --name $spoke1_vnet_name -o none
spoke2_vnet_id=$(az network vnet show -n $spoke2_vnet_name -g $rg --query id -o tsv)
az dns-resolver vnet-link create --ruleset-name $fwd_ruleset_name \
        --id "$spoke2_vnet_id" -g $rg --name $spoke2_vnet_name -o none
hub_vnet_id=$(az network vnet show -n $vnet_name -g $rg --query id -o tsv)
az dns-resolver vnet-link create --ruleset-name $fwd_ruleset_name \
        --id "$hub_vnet_id" -g $rg --name $vnet_name -o none

Reference: Diagnostic commands

All commands require an Azure extension. Here an example of a command that will trigger the installation of the extension:

❯ az dns-resolver inbound-endpoint list --dns-resolver-name $resolver_name -g $rg -o table
The command requires the extension dns-resolver. Do you want to install it now? The command will continue to run after the extension is installed. (Y/n): Y
Run 'az config set extension.use_dynamic_install=yes_without_prompt' to allow installing extensions without prompt.

All commands in this page have been tested with version 0.2.0 of the extension. If you have a more modern version, it can be that your outputs are different:

❯ az extension show -n dns-resolver -o table
ExtensionType    Name          Path                                          Version
---------------  ------------  --------------------------------------------  ---------
whl              dns-resolver  /home/jose/.azure/cliextensions/dns-resolver  0.2.0

The first example of diagnostic commands is of course the list of private resolvers in a resource group:

❯ az dns-resolver list -g $rg -o table
DnsResolverState    Location    Name         ProvisioningState    ResourceGroup    ResourceGuid
------------------  ----------  -----------  -------------------  ---------------  ------------------------------------
Connected           westeurope  hubresolver  Succeeded            dns              1ab3d593-16f4-4a6f-bfed-ffacdb9f2b9e

Once you know the name of your private resolver, you can show its endpoints:

❯ az dns-resolver inbound-endpoint list --dns-resolver-name $resolver_name -g $rg -o table
Location    Name    ProvisioningState    ResourceGroup    ResourceGuid
----------  ------  -------------------  ---------------  ------------------------------------
westeurope  hubin   Succeeded            dns              43ae0169-bd8b-48df-a17b-84551c19c25f

A problem with the previous command is that it doesn’t show the allocated IP address to the endpoint, so we can use a query with a JMES expression for that:

❯ az dns-resolver inbound-endpoint list --dns-resolver-name $resolver_name -g $rg --query '[].{Name:name,Location:location,IP:ipConfigurations[0].privateIpAddress}' -o table
Name    Location    IP
------  ----------  -------------
hubin   westeurope  192.168.101.4

You can have a look at the outbound endpoints too:

❯ az dns-resolver outbound-endpoint list --dns-resolver-name $resolver_name -g $rg -o table
Location    Name    ProvisioningState    ResourceGroup    ResourceGuid
----------  ------  -------------------  ---------------  ------------------------------------
westeurope  hubout  Succeeded            dns              ab614dda-1584-45a2-948c-f629a5735102

Or the forwarding rulesets:

❯ az dns-resolver forwarding-ruleset list -g $rg -o table
Location    Name           ProvisioningState    ResourceGroup    ResourceGuid
----------  -------------  -------------------  ---------------  ------------------------------------
westeurope  hubfwdruleset  Succeeded            dns              24aca0fe-37bc-472e-899a-2abea47cd779

Again, the previous command is missing some crucial information, namely the outbound point the ruleset is associated to. This command will show the first outbound endpoint association:

❯ az dns-resolver forwarding-ruleset list -g $rg --query '[].{Name:name,OutboundEndpoint:dnsResolverOutboundEndpoints[0].id,Location:location}' -o table
Name           OutboundEndpoint                                                                                                                                      Location
-------------  ----------------------------------------------------------------------------------------------------------------------------------------------------  ----------
hubfwdruleset  /subscriptions/e7da9914-9b05-4891-893c-546cb7b0422e/resourceGroups/dns/providers/Microsoft.Network/dnsResolvers/hubresolver/outboundEndpoints/hubout  westeurope

Once you have identified the ruleset, you can show the rules inside of it:

❯ az dns-resolver forwarding-rule list --ruleset-name $fwd_ruleset_name -g $rg -o table
DomainName    ForwardingRuleState    Name         ProvisioningState    ResourceGroup
------------  ---------------------  -----------  -------------------  ---------------
contoso.com.  Enabled                contosocom   Succeeded            dns

And here again, with the help of a bit of JMES you can show the first target DNS server configured:

❯ az dns-resolver forwarding-rule list --ruleset-name $fwd_ruleset_name -g $rg --query '[].{Name:name,DomainName:domainName,State:forwardingRuleState,TargetDnsServer:targetDnsServers[0].ipAddress}' -o table
Name         DomainName    State    TargetDnsServer
-----------  ------------  -------  -----------------
contosocom   contoso.com.  Enabled  172.16.0.4

And finally, you can check to what VNets the forwarding ruleset is linked to:

❯ az dns-resolver vnet-link list --ruleset-name $fwd_ruleset_name -g $rg -o table
Name    ProvisioningState    ResourceGroup
------  -------------------  ---------------
hub     Succeeded            dns
spoke1  Succeeded            dns
spoke2  Succeeded            dns

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: