An Intro to Terraform with Azure, PFSense, and Windows 10

I’ve recently begun using Terraform to automate infrastructure deployment as both a means to save time and ensure the systems are configured in the manner we specify. Prior to utilizing Terraform, I researched other methods to automate infrastructure deployment, but chose Terraform based on the described ease of using or switching between multiple providers (Azure, AWS, DigitalOcean, etc.).

To test for future lab deployments, I wanted to be able to deploy a Windows 10 VM within Azure and to place it within a private subnet making it inaccessible directly from the Internet. To access the Windows 10 VM, I wanted a second VM running PFSense so I can both have an OpenVPN service running on it, and use PFSense to create DNS records for clients accessing over the VPN and hosts within the private subnet. I wanted to document this specific use case for Terraform and Azure in case it helps anyone else trying to perform these same steps (in particular when it comes to deploying Windows 10 VMs in Azure via Terraform).

When using Terraform, you will likely have 3 primary files (potentially more if using custom modules, etc.), (your main code file), (a file containing variables referenced throughout your code), and terraform.tfvars (this will contain sensitive data, such as private API keys, etc.). To start using Terraform with Azure, you will likely want to install the Azure CLI tool and create an Azure service principal account (Microsoft has already documented this process). After you have set up your environment it’s time to start looking to create “infrastructure as code”.

In order to deploy these VMs, there are multiple dependencies that we will need to create as well, including:

  • A resource group
  • The overall network our subnet will reside within
  • Subnet which will contain the VMs
  • Private IPs, and a single public IP
  • OS disks
  • The virtual machines themselves
  • Local admin username and password
  • Security rules for restricting access to the PFSense system

The good thing is we can do all of the above with Terraform! Let’s start looking at some code!

tldr; Here’s a link to the whole script that will be discussed in this post.

Resource Groups and Networks

Resource Groups and Networks

The first code block (azurerm_resource_group) creates a new resource group within Azure called “private-net-group” within the US West region. The nice thing about Terraform is we can reference the resource group name and location via their respective variables, which makes resource group location or name changes simple because we only need to change the code in one place. To reference this information (which we will for the virtual network and subnets that will be created), you will need a combination of what it is that is being created, and the name you gave it in the terraform code:

  • Resource Group Name = ${azurerm_resource_group.<RESOURCEGROUP-TERRAFORMNAME>.name}
  • In our code – ${}
  • Resource Group Location = ${azurerm_resource_group.<RESOURCEGROUP-TERRAFORMNAME>.location
  • In our code – ${azurerm_resource_group.privatenetgroup.location}

The second block of code (azurerm_virtual_network) creates the overall virtual network that all subnets will reside in. This block is fairly self explanatory, it just needs a name for the virtual network, the overall address space, and a reference to both the resource group’s name and location that the virtual network will be deployed in.

The third block of code (azurerm_subnet) will create a subnet within the virtual network that the VMs will use and have network addresses within it. This also is relatively self-explanatory because it just needs a name for the subnet, a reference to the overall resource group’s name, and the subnet network range. The only difference with the azurerm_subnet is that it also requires a reference to the overall virtual network that it is within (which is the virtual_network_name parameter).

Finally, the fourth code block (azurerm_public_ip) is where we are also requesting a public IP. This block of code is very similar to the azurerm_subnet with the only difference being that we are requesting a static public IP, rather than a dynamic IP.

Security Group Configuration

Security Groups

The security group code block allows us to define rules that dictate what ports and protocols are accessible from the Internet, or internal sources. The first part of the code block contains the “standard” information, the name of the network security group, the location that it is deployed in, and the name of the resource group that it is a part of. The “security_rule”s are the meat of this section, it’s where you define the ports and protocols that are allowed, along with the priority of each rule. In the image above, we are allowing HTTPS (443/tcp) traffic and OpenVPN (1194/udp). When it comes to creating the actual virtual machines, this security group rule set will only be applied to the PFSense virtual machine which will allow us to access the VM over HTTPS to configure the PFSense system, and then over OpenVPN once the VPN server is configured and active.

Windows VM Configurations

VM Configuration

The next step in the Terraform deployment script is to create the VMs, and as you can see from the above image, there’s a decent amount of configuration options which are required. The first section (azurerm_network_interface) is again largely standard by asking for a name for the VM, the resource group name and location, but it also asks for a DNS name to associate with the VM.

The second section (ip_configuration) is in relation to networking with the Windows 10 VM. We’re providing a name for the IP configuration, specifying that we want a static private IP, and then specifying the specific IP address to associate with the Window 10 VM. We’re also specifying a reference to the specific subnet that the Windows 10 VM is joined to.

The third section (azurerm_virtual_machine) defines different attributes for the Windows 10 VM. First, the name of the virtual machine is given and we’re also providing a reference to the resource group location and name that the virtual machine is joined to. The “network_interface_ids” property is a reference to the network interface created for the Windows 10 virtual machine and its associated static IP address. Finally, the the “vm_size” property specifies the VM size that we can allocate within Azure and the “delete_os_disk_on_temination” will delete the hard drive when we chose to delete the virtual machine itself.

The fourth block (storage_image_reference) is a code block that was harder for me to find. This section specifies the specific VM image that Terraform and Azure will deploy, however finding this information proves more difficult. If you search for examples to deploy Windows systems in Azure with Terraform, the only examples you will find are Linux VMs or Windows Server VMs. I have not found any sample Windows 10 terraform deployment scripts, and had a hard time identifying the specific information required to deploy a Windows 10 image via Terraform. I was very fortunate that Lee Holmes knew of an easy way to identify Windows 10 SKUs within Azure, and he provided an Azure CLI one-liner that you can use to find supported SKUs.

Windows 10 SKU Search

This helped to identify different Windows 10 virtual machines that are supported by Azure. I wanted to see if there were any newer versions of Windows 10 supported. So based off of Lee’s search terms, I did another search for RS4 Win 1o SKUs, and found the information that’s used in the Terraform file.


The fifth code block (storage_os_disk) contains information about the hard drive we want to use with the Windows 10 VM. The main attributes that are needed are a name for the hard drive, the caching type, the create_option, and the managed disk choice. For most deployments, you can use these options that are shown in the image, minus changing the name of the disk itself.

The sixth code block (os_profile_windows_config) just specifies specific configuration options for Windows VMs. In this instance, we’re configuring the VM to not enable automatic updates, but allowing it to install the Azure Virtual Machine Guest Agent.

The final code block is also self-explanatory in that we are specifying the computer name, the local administrator username, and the local administrator password.

PFSense VM Configuration

PFSense Configuration

To prevent redundant information, I’m not going to cover all code blocks which are the same from Windows to PFSense. The similar code blocks include the “azurerm_virtual_machine”, “azurerm_network_interface”, “storage_os_disk”, and “os_profile”. The “storage_image_reference” information can be identified by searching for “pfsense”.

PFSense SK

The “plan” block is required to use a pre-built image within Azure. I could not find a way to identify this information via the Azure CLI (but I would love feedback if someone has a method to capture this information), I could only find the plan information via the Azure Portal. As I generated the PFSense virtual machine within the Azure portal, I could find the relevant plan information within the template information.

Azure Plan

I used this information to populate the “plan” information code block. This is required in order to deploy a PFSense system via within Azure.

The “os_profile_linux_config” replaced the “os_profile_windows_config”code block and we only needed to provide one property. Since we didn’t provide a SSH key, the other option to authenticate is via password, so we chose to not disable it.


Thankfully, Twitter has helped to solve this issue on how to identify the plan information associated with the PFSense image (and this process could be applied to other images as well).

When we run the above command via the Azure CLI, you can easily see that the plan information is given to us.

Plan for PFSense

Thanks for the info @K12sec!

Deploying with Terraform

Now that we’ve created the code for deploying our systems within Azure, it’s time to test deploying it, and then actually creating the virtual infrastructure. The first step is to run the command “terraform init” to download the dependencies. After running that, the next step is “terraform plan” which will quickly parse the file for any syntax or logic issues that may exist. Assuming your code has no issues, you should see something similar to the following image:

Deploying with PFSense

The “terraform plan” command does not actually deploy your requested resources. To actually deploy the virtual resources, you will want to run the “terraform apply” command. You will be asked if you want to deploy the resources, and after that, Terraform will deploy your virtual machines, networks, and more.

Give Terraform about 10 minutes, and you should have everything deployed!

Hopefully this helps this provides valuable insight on how to deploy a Windows 10 and PFSense VM within Azure with Terraform. If you have any questions, please don’t hesitate to contact us at FortyNorth Security!