CI/CD setups use a lot of automation to achieve their goals of improving development pace and quality. Automated agents are used to execute tasks such as building code changes, executing tests, and deploying increments.
Provisioning and maintaining these automated agents is often a tedious job; therefore, platforms such as Azure DevOps offer their users the possibility to use “hosted” agents that are provisioned automatically when a job needs to run and are destroyed afterwards. However, if a job needs to access other cloud resources (such as databases or virtual machines), making this access secure might be a problem.
This exact topic arose on one of our current projects. The project has been under development for some time and historically used self-hosted build agents running in our local infrastructure. This worked quite well. However, as part of the company-wide cloud push, the project members decided to migrate all the local development infrastructure (database servers, application hosts, build agents) to Microsoft Azure.
In order to follow best practices, it was decided that these resources would be provisioned within their own virtual network (v-LAN) and access to the network from the Internet would be entirely disabled. This means that a VPN connection to the v-LAN would need to be established by anyone wanting to access the project’s cloud resources. This includes the automated build agents used in the project’s CI/CD pipelines. Since one of the build steps is to execute a set of integration tests (this requires access to a database server in the cloud), the build agents must be able to establish the VPN connection on their own.
The issue with Azure VPN
Azure is well-equipped for the scenario outlined above. We created our v-LAN, provisioned a few virtual machines and other resources in it, established a VPN gateway in the network, and cut off all public access to the resources within the network. Next, we tested the VPN access.
There are two main modes of operation for an Azure VPN gateway: site-to-site and point-to-site. The site-to-site connection is usually established on a site level and works seamlessly for all users on that site. The point-to-site VPN connection only connects a single user. For reasons beyond the scope of this article, we could not use the site-to-site VPN connection and had to use the point-to-site connection mode.
The Azure VPN gateway allows users to download a small installer package that creates and configures the necessary P2S VPN settings automatically (on Windows). This works quite seamlessly for people who want to be able to connect to the virtual network. Unfortunately, the configuration created is such that in order to connect to the VPN, user action and administrative rights are necessary. This is not a problem for real humans but presents quite an obstacle for non-interactive environments such as build agents.
In the example scenario above, we have a hosted build agent that lives “in the cloud”, is non-interactive, and must establish a P2S VPN connection to an Azure v-LAN. This turned out to be trickier than originally expected.
Establishing an Azure P2S VPN connection from the command line
After a lot of research and experimentation, we realised there is no officially supported way to achieve the P2S connection from the command line without a lot of fussing with network configuration. Fortunately, the Azure VPN installer mentioned above provides a way to bypass this limitation with just a few commands.
The whole “trick” lies in the Windows utility `rasdial` and some creative usage of the Azure VPN installer.
Rasdial is a Windows command line utility that can connect and disconnect VPN connections. Unfortunately, just pointing it at the Azure VPN gateway isn’t enough because rasdial doesn’t support client certificate authentication (which Azure VPN requires).
The key to side-stepping this limitation is provided in the Azure P2S VPN installer itself. This installer exe file is actually an archive that can be extracted:
The archive contains several files required by the installation process. The most important file is the PBK file. This is a “Dial-Up Phonebook” file containing all the information necessary for rasdial to establish a VPN connection. It also contains information about the authentication mode – in the case of Azure VPN, it specifies which certificates are usable for authenticating to the VPN gateway. The file is named after the GUID assigned to your Azure VPN gateway.
The other important file in the archive is the routes.txt file. This file describes the routing rules necessary for the VPN connection to work properly (basically which connections should go through the VPN and which should not).
ADD 192.168.1.0 MASK 255.255.255.0 default METRIC default IF default ADD 172.16.0.0 MASK 255.255.255.0 default METRIC default IF default
With these files, we can start creating a script that will establish a P2S connection automatically. These are the steps necessary for it to work on a build agent with no prior configuration:
1. Install a client certificate (this will be used automatically to authenticate against the VPN gateway)
2. Invoke rasdial using the PBK file from the Azure installer
3. Add the routing rules from the routes.txt file
4. The VPN tunnel is established and configured; you can access your cloud resources securely
5. Disconnect the VPN tunnel
The PowerShell script to handle all of this is rather short (the example assumes the VPN gateway GUID is 11111111-1111-1111-1111-111111111111):
$pass = ConvertTo-SecureString -string “MyPassword” -Force -AsPlainText Import-PfxCertificate -FilePath client.pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pass rasdial "11111111-1111-1111-1111-111111111111" /PHONEBOOK:"./ 11111111-1111-1111-1111-111111111111.pbk" New-NetRoute -DestinationPrefix "172.16.0.0/24" -InterfaceAlias 11111111-1111-1111-1111-111111111111 New-NetRoute -DestinationPrefix "192.168.1.0/24" -InterfaceAlias 11111111-1111-1111-1111-111111111111
To disconnect the VPN tunnel, simply call rasdial with the DISCONNECT parameter:
rasdial "11111111-1111-1111-1111-111111111111" /DISCONNECT
This sample script uses a client certificate stored in a password-protected PFX file, but other sources could be used (such as Azure KeyVault). It’s also worth pointing out that the route registration does not directly use the routes.txt file. Instead, we decided to simply rewrite the registration using PowerShell commands. Your routes might be different.
Another important point is that this script contains a lot of secrets. When using this in productive setup, these should come from secure sources (such as KeyVault, secure build pipeline variables, etc.).
And that’s it. With this script, we can now connect any build agent (or any unattended machine) to our Azure virtual network in an automated manner using a P2S VPN connection. The agent/machine can then access protected cloud resources securely and without having to expose them to the public Internet.
In this article, I explained a problem with accessing Azure virtual networks using point-to-site VPN connections without user interaction. I then described a way to solve this problem using rasdial and the VPN configuration installer produced by Azure. In the end, I created a self-contained PowerShell script that can establish an Azure P2S VPN connection in build agents and other unattended setups.