What is Always On VPN?
Always On VPN is the recommended replacement for Microsoft’s DirectAccess, it is also a Microsoft Product that allows you have a constant VPN connection to a selected network.
In this blog, I am going to show you how you can use an Always On device based VPN setup utilising an Azure VPN Gateway
Requirements & Restrictions
- Always On VPN can be configured either device (device certificate) or user based when using an Azure VPN Gateway
- Windows 10 Enterprise requirement for user devices
- The Azure VPN Gateway must be route-based configuration
- Azure VPN Gateway SKU must be VpnGw1 or above, basic Gateway is not supported
- Note the maximum connections on each Gateway limitation (You may require more for your setup, will include the common 3 Gateways below
- The device must be a domain joined computer running Windows 10 Enterprise or Education version 1809 or later.
- The tunnel is only configurable for the Windows built-in VPN solution and is established using IKEv2 with computer certificate authentication.
- Only one device tunnel can be configured per device.
Vpn Gateway | Bandwidth | P2S Tunnels |
VPNGw1 | 650 Mbps | Max 250 1-128: Included 129-250: £0.008/hour per connection |
VPNGw2 | 1 Gbps | Max 500 1-128: Included 129-500: £0.008/hour per connection |
VPNGw3 | 1.25 Gbps | Max 1000 1-128: Included 129-1000: £0.008/hour per connection |
Why will the Azure VPN Gateway be used for?
An Always On VPN device tunnel is a certificate-based authentication, the Always On VPN device tunnel is authenticated against a certificate CA that is issued on your VPN Gateway. The VPN Gateway will then authorise a successful connection if the user’s certificate matches with the CA.
What certificates to use?
In this blog, I am using self-signed certificates but in an actual production-like environment a verified CA would be recommended
Create self-signed certificates as below
Create a Root CA and Client self-signed certificates
#Create Certs - Root
$tamopsrootcert = New-SelfSignedCertificate -Type Custom -KeySpec Signature `
-Subject "CN=tamopsvpnrootcert" -KeyExportPolicy Exportable `
-HashAlgorithm sha256 -KeyLength 2048 `
-CertStoreLocation "Cert:CurrentUserMy" -KeyUsageProperty Sign -KeyUsage CertSign
#Create Certs - Client
New-SelfSignedCertificate -Type Custom -DnsName P2SChildCert -KeySpec Signature `
-Subject "CN=P2SChildCert" -KeyExportPolicy Exportable `
-HashAlgorithm sha256 -KeyLength 2048 `
-CertStoreLocation "Cert:CurrentUserMy" `
-Signer $tamopsrootcert -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2")
Both certificates now available in your Personal Certificate store of current user

Configure Point-to-Site Configuration on Azure VPN Gateway
Address Pool:- Needs to be configured, this pool is the IP Address that connected VPN traffic source will be coming from
Tunnel Type:- IKEv2 and OpenVPN (SSL) or IKEv2
Upload Root Certificate created above public key to the Azure VPN Gateway
PowerShell script below to achieve these changes
#Extract Root Cert
$certfind = Get-ChildItem -Path Cert:CurrentUserMy | ?{$_.Subject -eq 'CN=tamopsvpnrootcert'}
export-Certificate -cert $certfind -FilePath C:UsersThomasDesktopexportcert.cer -type CERT -NoClobber
certutil -encode C:UsersThomasDesktopexportcert.cer C:UsersThomasDesktopuseme.cer
#Upload configuration changes to Azure VPN Gateway
$P2SRootCertName = "P2SRootCert.cer"
$filePathForCert = "C:UsersThomasDesktopuseme.cer"
$cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2($filePathForCert)
$CertBase64 = [system.convert]::ToBase64String($cert.RawData)
$p2srootcert = New-AzVpnClientRootCertificate -Name $P2SRootCertName -PublicCertData $CertBase64
Add-AzVpnClientRootCertificate -VpnClientRootCertificateName $P2SRootCertName -VirtualNetworkGatewayname "tamopsvpngw" -ResourceGroupName "vnet-vpn" -PublicCertData $CertBase64
Create VPN profile
Always On VPN profile example taken from docs.microsoft
<VPNProfile>
<NativeProfile>
<Servers>azuregateway-1234-56-78dc.cloudapp.net</Servers>
<NativeProtocolType>IKEv2</NativeProtocolType>
<Authentication>
<MachineMethod>Certificate</MachineMethod>
</Authentication>
<RoutingPolicyType>SplitTunnel</RoutingPolicyType>
<!-- disable the addition of a class based route for the assigned IP address on the VPN interface -->
<DisableClassBasedDefaultRoute>true</DisableClassBasedDefaultRoute>
</NativeProfile>
<!-- use host routes(/32) to prevent routing conflicts -->
<Route>
<Address>192.168.3.5</Address>
<PrefixSize>32</PrefixSize>
</Route>
<Route>
<Address>192.168.3.4</Address>
<PrefixSize>32</PrefixSize>
</Route>
<!-- need to specify always on = true -->
<AlwaysOn>true</AlwaysOn>
<!-- new node to specify that this is a device tunnel -->
<DeviceTunnel>true</DeviceTunnel>
<!--new node to register client IP address in DNS to enable manage out -->
<RegisterDNS>true</RegisterDNS>
</VPNProfile>
From this VPN profile, modify <servers> to include your Azure Gateway Address. This is found by downloading the VPN client from Azure VPN Gateway

Once downloaded, extract the .zip and inside General folder, review the VPNSettings.xml

Reviewing .xml you will find <VpnServer>, this is to be updated inside your VPNProfile.xml
<VpnServer>azuregateway-6f0b0078-26a7-4fca-9dad-34fdfe627981-c9b472fa6a8e.vpn.azure.com</VpnServer>
Update <Route> to specific routes you want your Always On VPN connection to access. For this example, I have include the full vNET
<Route>
<Address>10.0.1.0</Address>
<PrefixSize>24</PrefixSize>
</Route>
Final VPNProfile.xml
<VPNProfile>
<NativeProfile>
<Servers>azuregateway-6f0b0078-26a7-4fca-9dad-34fdfe627981-c9b472fa6a8e.vpn.azure.com</Servers>
<NativeProtocolType>IKEv2</NativeProtocolType>
<Authentication>
<MachineMethod>Certificate</MachineMethod>
</Authentication>
<RoutingPolicyType>SplitTunnel</RoutingPolicyType>
<!-- disable the addition of a class based route for the assigned IP address on the VPN interface -->
<DisableClassBasedDefaultRoute>true</DisableClassBasedDefaultRoute>
</NativeProfile>
<!-- use host routes(/32) to prevent routing conflicts -->
<Route>
<Address>10.0.1.0</Address>
<PrefixSize>24</PrefixSize>
</Route>
<!-- need to specify always on = true -->
<AlwaysOn>true</AlwaysOn>
<!-- new node to specify that this is a device tunnel -->
<DeviceTunnel>true</DeviceTunnel>
<!--new node to register client IP address in DNS to enable manage out -->
<RegisterDNS>true</RegisterDNS>
</VPNProfile>
Further details on building a VPN Profile in this docs.microsoft article
Creating and deploying Always On VPN Profile
VPNProfile.xml to be exported in format below using this PowerShell
$ProfileXML = Get-Content C:UserstamopsdesktopVPNProfile.xml
# Escape spaces in profile name
$ProfileNameEscaped = $ProfileName -replace ' ', '%20'
$ProfileXML = $ProfileXML -replace '<', '<'
$ProfileXML = $ProfileXML -replace '>', '>'
$ProfileXML = $ProfileXML -replace '"', '"'
$ProfileXML | Out-File -FilePath ($env:USERPROFILE + 'desktopVPN_Profile.xml')
Now time to deploy your VPN Always On Profile, in my example I am deploying onto a test Virtual Machine using PowerShell with PsEXEC as below:-
PsExec.exe -i -s C:windowssystem32WindowsPowerShellv1.0powershell.exe
Take the output of VPN_Profile.xml and add into variable $ProfileXML as below, this is the script you will be running with PsExec.exe
$ProfileName = 'TamOps Always On VPN'
$ProfileXML = '<VPNProfile>
<NativeProfile>
<Servers>azuregateway-6f0b0078-26a7-4fca-9dad-34fdfe627981-c9b472fa6a8e.vpn.azure.com</Servers>
<NativeProtocolType>IKEv2</NativeProtocolType>
<Authentication>
<MachineMethod>Certificate</MachineMethod>
</Authentication>
<RoutingPolicyType>SplitTunnel</RoutingPolicyType>
<!-- disable the addition of a class based route for the assigned IP address on the VPN interface -->
<DisableClassBasedDefaultRoute>true</DisableClassBasedDefaultRoute>
</NativeProfile>
<!-- use host routes(/32) to prevent routing conflicts -->
<Route>
<Address>10.0.1.0</Address>
<PrefixSize>24</PrefixSize>
</Route>
<!-- need to specify always on = true -->
<AlwaysOn>true</AlwaysOn>
<!-- new node to specify that this is a device tunnel -->
<DeviceTunnel>true</DeviceTunnel>
<!--new node to register client IP address in DNS to enable manage out -->
<RegisterDNS>true</RegisterDNS>
</VPNProfile>'
$ProfileNameEscaped = $ProfileName -replace ' ', '%20'
$ProfileXML = $ProfileXML -replace '<', '<'
$ProfileXML = $ProfileXML -replace '>', '>'
$ProfileXML = $ProfileXML -replace '"', '"'
$nodeCSPURI = './Vendor/MSFT/VPNv2'
$namespaceName = "rootcimv2mdmdmmap"
$className = "MDM_VPNv2_01"
$session = New-CimSession
try {
$newInstance = New-Object Microsoft.Management.Infrastructure.CimInstance $className, $namespaceName
$property = [Microsoft.Management.Infrastructure.CimProperty]::Create("ParentID", "$nodeCSPURI", 'String', 'Key')
$newInstance.CimInstanceProperties.Add($property)
$property = [Microsoft.Management.Infrastructure.CimProperty]::Create("InstanceID", "$ProfileNameEscaped", 'String', 'Key')
$newInstance.CimInstanceProperties.Add($property)
$property = [Microsoft.Management.Infrastructure.CimProperty]::Create("ProfileXML", "$ProfileXML", 'String', 'Property')
$newInstance.CimInstanceProperties.Add($property)
$session.CreateInstance($namespaceName, $newInstance)
$Message = "Created $ProfileName profile."
Write-Host "$Message"
}
catch [Exception] {
$Message = "Unable to create $ProfileName profile: $_"
Write-Host "$Message"
exit
}
$Message = "Complete."
Write-Host "$Message"

You will now have a successful Always On VPN connection
Also note, the certificates created/used need to be in personal/TrustedRoot (CA cert) stores of your device.
Have you found a way to make this work with Azure AD authentication that uses the users credentials at logon to connect?
Hi Gary, you may want to incorporate an NPS VM setup along with this to handle the Azure AD Auth? Depending on your initial requirements
It looks like many of your code snippets have lost their “\”
Thanks Ben, you notice on just this blog post only?
This guide really helped me to set this up outside of all of the missing characters in the scripts due to how WordPress screws up formatting. I kept getting “Unable to create Always On VPN profile: A general error occurred that is not covered by a more specific error code and thought it was my XML profile, but it turns out it was due to the script snippet from this site missing characters (the 3 lines that start with “$ProfileXML = $ProfileXML -replace”). Once I figured that out, the profile was created, and I was able to connect immediately.