Skip to content

Accessing Resources via Private Endpoint in Azure Hub-and-Spoke Virtual Network with Basic SKU VPN Gateway

In this blog post, we’ll be:

  • configuring a virtual network topology in Azure in the “hub and spoke model”
  • deploying an example resource (a Key Vault) in our spoke network
    • restricting access to the Key Vault using a private endpoint connection so that it is only accessible inside the vnet
  • configuring a DNS forwarder running Debian + Unbound in the hub network for resolving the private DNS name of the Key Vault
  • configuring a Basic SKU Virtual Network Gateway
  • configuring a Windows client to connect to the Basic VPN Gateway in a point-to-site configuration so it has access to the Key Vault through the private endpoint

Diagram of the architecture. A key vault, KVNEHubAndSpokeTest, is at the left of the diagram, connected to a virtual network vnetHSTestDev (172.16.1.0/24). This is peered with vnetHSTestConnectivity (172.16.0.0/24). This vnet contains private DNS zones, a virtual machine VM-NE-ConnectivityDNS (172.16.0.4), and the basic SKU virtual network gateway, vpng-HSTestConnectivity. On the right, the internet, and a VPN client connected through it. The VPN client has a line connecting it, via the internet, to the VPN gateway

Why?

A hub and spoke network with private endpoints for restricting access to various Azure PaaS resources is a fairly common architecture, but there are a few parts of it that lead to unnecessary costs: namely the PaaS private DNS resolver and the Virtual Network Gateway in its non-Basic SKUs, such as VpnGw1.

The primary purpose of this post is to document how I’ve achieved this architecture using the Virtual Network Gateway Basic SKU, which saves ~£80/month over the VpnGw1 SKU. It also saves the PaaS private DNS resolver costs by using a lightweight VM.

Create hub network

We’ll start by creating our “hub” network, called vnetHSTestConnectivity in my case.

Create virtual network screen. The virtual network name is vnetHSTestConnectivity

We’ll be using the 172.16.0.0/24 range for this network.

Create virtual network IP addressing configuration. We have chosen 172.16.0.0/24

We will make the default subnet smaller, a /26, and add a gateway subnet after it:

Add a subnet: we are adding 172.16.0.64/27 as the GatewaySubnet. The default subnet is configured to 172.16.0.0/26

Subnet Name

Range

default

172.16.0.0/26

GatewaySubnet

172.16.0.64/27

Create “Dev” spoke network

Our spoke network will be considered the “Dev” network, and called vnetHSTestDev.

Create virtual network screen. It is called vnetHSTestDev

Here, we’ll use the next /24 block for IP addressing, 172.16.1.0/24.

IP address screen. We are using 172.16.1.0/24

Peer the Connectivity (hub) and Dev (spoke) networks

We need to peer the two networks. In the vnet configuration, go to Peerings and Add:

Peering overview screen, showing no peerings yet configured

We need to do this on both ends of the connection, so for both vnets:

peerConnectivityDev, showing the local virtual network peering settingspeerDevConnectivity, showing the local virtual network peering settings

Deploy a Basic SKU VPN Gateway in the Connectivity (Hub) network

The Basic SKU cannot be selected in the Azure Portal UI, so we’ll need to deploy by PowerShell. Let’s deploy this via the cloud shell. We will need to select switch to PowerShell if the shell starts in bash.

Azure cloud PowerShell, running ./build-vpngw.ps1

The script to run is in my GitHub repository for this post, and is adapted from this comment in the azure-docs repository (thanks to Gitarani Sharma).

(I quickly used vim to paste in the script contents and run it, but you can also use the cloud shell’s Manage files > Upload to get the PowerShell script into the cloud shell environment so that you can run it.)

Azure cloud PowerShell, showing verbose progress "performing the operation Creating Resource"...

Build a VPN client machine

The machine that will connect into this virtual network and access the key vault will also be another Azure VM. This doesn’t need to be in Azure at all — I’ve done it just for convenience’s sake; it’s a mighty quick way to build a Windows VM! In a real environment, this is likely to be your development machine that you’re using to develop whatever Azure solution you’re building.

Note that the VPN client machine lives entirely outside the vnets we have created — the point is that this machine is remote and connects via the VPN!

Create virtual machine screen, showing no infrastructure redundancy required and the smalldisk Windows Server 2025 image

Generate VPN Gateway certificates

For the purposes of authenticating to the VPN Gateway, we will use self-signed certificates as per Microsoft’s guidance. In a real environment, of course, we would want to use a proper PKI!

We will misuse the VPN client machine to generate these for our lab environment. In PowerShell:

$params = @{
Type = 'Custom'
Subject = 'CN=P2SRootCert'
KeySpec = 'Signature'
KeyExportPolicy = 'Exportable'
KeyUsage = 'CertSign'
KeyUsageProperty = 'Sign'
KeyLength = 2048
HashAlgorithm = 'sha256'
NotAfter = (Get-Date).AddMonths(24)
CertStoreLocation = 'Cert:\CurrentUser\My'
}
$cert = New-SelfSignedCertificate @params

$params = @{
Type = 'Custom'
Subject = 'CN=P2SChildCert'
DnsName = 'P2SChildCert'
KeySpec = 'Signature'
KeyExportPolicy = 'Exportable'
KeyLength = 2048
HashAlgorithm = 'sha256'
NotAfter = (Get-Date).AddMonths(18)
CertStoreLocation = 'Cert:\CurrentUser\My'
Signer = $cert
TextExtension = @(
'2.5.29.37={text}1.3.6.1.5.5.7.3.2')
}
New-SelfSignedCertificate @params

Make a note of the 24 month and 18 month certificate expiry, if this environment is likely to last that long!

Now, we need to export the P2SRootCert root certificate. Open mmc and add the Certificates snap-in for the current user.

 

 

MMC certificates snap-in, focusing on the context menu of P2SRootCert, where we are about to click Export

We don’t need to export the private key in this case, as we won’t be signing any new certificates or using this PKI outside of this test environment.

Export as base 64 format:

Export certificate wizard, showing us selecting Base-64 encoded X.509 (.cer)

Open the exported file in Notepad and copy the base 64 certificate content. Exclude the beginning and end lines; you should have just the base 64 characters themselves.

Set up VPN Gateway point-to-site configuration

VPN Gateway point-to-site configuration -- not yet configured

The address pool needs to be a range specifically for VPN clients and must be outside the ranges of the networks that those clients seek to access through the VPN.

We will choose 192.168.240.0/24, as this is well outside the 172.16 ranges we are using for the hub and spoke networks, and is less likely to conflict with domestic local networks that use low numbers in the 192.168.* space!

Paste the base64 content of the root certificate into public certificate data and give it a name.

Point-to-site configuration, showing address pool 192.168.240.0/24 and the P2SRootCert base 64 content pasted in to Public certificate data.

Add the additional routes of the peered networks to Additional routes to advertise. In our case, that’s the IP range of the Dev network, 172.16.1.0/24.

Additional routes to advertise, showing 172.16.1.0/24

This may take some time to save the new configuration, so while we wait for Updating to disappear from the VPN Gateway’s Overview page, we’ll set up our private DNS zone for the Key Vault. (We haven’t created the vault yet!)

Create private DNS zone for Key Vault

Deploy a new Private DNS Zone. We don’t need to do anything in the Private DNS Zone Editor, just deploy it as is:

Create Private DNS Zone screen, with instance name privatelink.vault.azure.net

Link the Private DNS Zone to the connectivity (hub) network:

Add Virtual Network Link to Private DNS zone, showing that it is linked to the vnet vnetHSTestConnectivity

Deploy DNS server in the Connectivity (hub) network

So that we can resolve the name of the Key Vault to its internal IP address in our Dev (spoke) network, we need a DNS server that’s in the Connectivity (hub) network.

The VPN client machine will query this DNS server for the specific namespaces that my connection script will configure, including .vault.azure.net.

You can of course do this with Azure Private DNS Resolver, but at a much higher cost!

I will deploy a Debian VM in this instance, and use the Unbound DNS server.

Create a virtual machine for the DNS resolver machine, showing name VM-NE-ConnectivityDNS, Debian 12 bookworm arm64 image and the size Standard_P2pts_v2

It will be deployed into the default subnet of vnetHSTestConnectivity:

Create a virtual machine networking screen for DNS resolver VM, showing the vnetHSTestConnectivity virtual network, default VLAN and no NIC network security group

Once deployed, connect over SSH to the new VM.

This new DNS server has been assigned 172.16.0.4 in my example.

We will install and configure Unbound:

sudo apt update
sudo systemctl disable --now systemd-resolved
sudo apt install unbound
sudo vim /etc/unbound/unbound.conf

 

I’ll edit the config file to add access-control entries for both the private ranges of the hub and spoke network, and also for the VPN connection IP address range 192.168.240.0/24.

Finally, the forward-zone stanza ensures that all DNS queries are forwarded via the Azure DNS resolver inside the Connectivity network. Because the private DNS zone is linked to this network, our DNS VM and its clients can resolve the private names.

server:
  interface: 0.0.0.0
  access-control: 172.16.0.0/16 allow
  access-control: 192.168.240.0/24 allow

forward-zone:
  name: "."
  forward-addr: 168.63.129.16

Unbound configuration, showing the access-control and forward-addr stanzas

Let’s restart Unbound:

sudo systemctl restart unbound

Deploy Key Vault with private link

Key Vault in the Marketplace. We are about to click Key Vault to create it

Create key vault screen, showing the name KVNEHubAndSpokeTestDev

My Key Vault is called KVNEHubAndSpokeTestDev.

 

Key Vault Access Config screen, where we leave the defaults

On the Networking tab, we will disable public access and create a private endpoint linked to vnetHSTestDev:

Key vault create private endpoint screen, showing that we are connecting the private endpoint to vnetHSTestDev

Once created, we will be unable to list keys or secrets in the portal, as we’re not yet transiting the VPN to access the Key Vault via the private endpoint:

Key Vault Keys screen, showing that "public network access is disabled and request is not from a trusted service nor via an approved private link"

Let’s make sure the user we are using has the appropriate RBAC permissions to work with the data in the Key Vault, and it’s only the private endpoint issue that’s preventing us from having access. (Even an Owner will not have access to the Key Vault data plane by default.)

We will add the Key Vault Administrator RBAC role:

RBAC screen for Key Vault, adding Key Vault Administrator

We need to link the private DNS zone for privatelink.vaultcore.azure.net into the Connectivity vnet:

Add Virtual Network Link for privatelink.vaultcore.azure.net into the vnetHSTestConnectivity network

Test: look up the Key Vault name on the DNS server

To test that the private DNS zone is working, I’ll log onto my DNS server VM and query it directly for the name of the Key Vault.

sudo apt install dnsutils
dig @127.0.0.1 kvnehubandspoketestdev.vault.azure.net

Using dig to resolve the Key Vault on the DNS server. We see the result of dig command -- an address of 172.16.1.5

We know this is working here as it resolves to a private IP address in the Dev network range: 172.16.1.5.

Connect from isolated VM into the connectivity network via the VNG

Everything is looking good, but we haven’t yet connected our VPN client to the network!

Back in the VPN Gateway Overview page, download the connection profile by pressing the Download VPN client button.

In the zip file that you’ll get, we will transfer the script in the WindowsPowerShell folder across to the VPN client machine.

Add VPN connection PowerShell script, showing that it begins with an embedded XML document with an EapHostConfig root node

Run this PowerShell script. If successful, you’ll be able to go to Windows Settings > Network > VPN and see the new profile vnetHSTestConnectivity. Don’t connect to it just yet.

Windows Settings app > network & internet > VPN, showing that a connection called vnetHSTestConnectivity exists, but is not connected

When we connect to the VPN, we need to also add rules so that Windows directs DNS requests for the private resources that need to be resolved inside the VPN to our DNS server in that network. This can be handled automatically by OpenVPN etc. using the higher end VPN Gateway SKU, but not the Basic SKU.

I’ve written a PowerShell script for connecting and disconnecting from the network that automates the management of these DNS policy rules to let us resolve the private names. Let’s take a look.

The scripts and configuration are in the GitHub repository that accompanies this post.

config.json contains the private DNS zones that will be resolved via the private DNS resolver, and the IP address of this VM inside the vnet.

{
    "VPNConfigurationName": "vnetHSTestConnectivity",
    "VPNGatewayRange": "192.168.240.0/24",
    "DNSClientNRPTRules": [
        ".vault.azure.net",
        ".table.core.windows.net",
        ".vaultcore.azure.net",
        ".servicebus.windows.net",
        ".queue.core.windows.net",
        ".file.core.windows.net",
        ".blob.core.windows.net",
        ".azurewebsites.net"
    ],
    "DNSServer": "172.16.0.4"
}

(We’re only using .vault.azure.net / .vaultcore.azure.net in this post, but this configuration is flexible!)

We will connect to the VPN by running Connect-BasicVPNGatewayWithPrivateDNS.ps1

PowerShell - Connect-BasicVPNGatewayWithPrivateDNS.ps1. The command has not yet run

PowerShell - Connect-BasicVPNGatewayWithPrivateDNS.ps1. We can see verbose output for rthe script adding various DNS policy rules "adding NRPT rule for namespace"...

We should now be connected to the VPN. We should be able to ping the DNS server at 172.16.0.4.

Let’s verify that we can resolve the private endpoint DNS name of the Key Vault to its internal IP address:

Resolve-DnsName kvnehubandspoketestdev.vault.azure.net

Reolsve-DnsName kvneuhubandspoketestdev.privatelink.vaultcore.azure.net resolves to 172.16.1.5
Great! We can see the IP4Address returned is in the spoke network range: 172.16.1.5.

Now let’s access the vault over the private endpoint by means of our VPN connection. For demonstration purposes, we will do this using a browser on our VPN client machine and accessing the Azure Portal.

Remember that before we saw this error?

Key Vault Keys screen, showing that "public network access is disabled and request is not from a trusted service nor via an approved private link"

We should now be able to generate a key:

Azure Key Vavult Generate Key screen. We are creating a key called test

and add a secret:

Azure Key Vault Create secret screen. We are creating a secret called testsecret

If we disconnect from the VPN, we should be unable to list the secrets again, due to the vnet restriction.

It’s important to use the corresponding disconnect script to disconnect, so that the DNS policy rules are removed and those Azure DNS namespaces don’t continue to try and resolve through an inaccessible DNS server!

.\Disconnect-BasicVPNGatewayWithPrivateDNS.ps1

Disconnect script, showing the removal of the DNS NRPT rules that were added by the connection script

Once disconnected from the VPN, we’re locked out of listing the secrets again!

Key Vault, again showing that we cannot list secrets

 

Like this post?

If you would like to support the time and effort I have put into my tutorials and writing, please consider making a donation.

Post a Comment

On some sites, you must be logged in to post a comment. This is not the case on this site.
Your email address is not made public or shared. Required fields are marked with *.
*
*
*

Posting a comment signifies you accept the privacy policy.
Please note — your comment will not appear straight away, as all comments are held for approval.