Terraform on Azure - Use a Network Security Group for a Windows Virtual Machine

azure cloud terraform infrastructureascode virtualmachine securitygroup ports hashicorp microsoft devops

This post is about how to establish a network security group for a Windows virtual machine using Terraform on Azure

1. Introduction

If you’re going to create virtual machines, then you can’t live without network security groups and other fundamental components as a virtual network. A network security group allows you to decide e.g.: which port or port range is exposed, respectively which port or port range and protocol is not available for network traffic. This can be achieved by defining security rules.

In this post, I’d like to show how to create a meaningful network security group for a Windows virtual machine.

2. Network Security Group - Example Usage

An example usage of a network security group can be found at the Terraform Registry:

registry.terraform.io/ - network security group

It consists of an azurerm_resource_group named “example” and of the azurerm_network_security_group, also named “example”. Be aware that this code snippet just serves as an example, which is not ready for production. The security rule named “test123” matches all ports by using the asterisk (*) for allowing access. That’s of course not a good idea from a security perspective, because just those specific ports should be opened, which are really mandatory to be exposed. For instance port 22 for allowing a SSH connection or port 3389 for allowing a RDP connection. If you’d like to run a specific service on the virtual machine, then you probably need to open further ports - depending on service or application which you would like to host.

resource "azurerm_resource_group" "example" {
name     = "example-resources"
location = "West Europe"
}

resource "azurerm_network_security_group" "example" {
name                = "acceptanceTestSecurityGroup1"
location            = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name

security_rule {
name                       = "test123"
priority                   = 100
direction                  = "Inbound"
access                     = "Allow"
protocol                   = "Tcp"
source_port_range          = "*"
destination_port_range     = "*"
}
}


The following section contains a full example of a Terraform configuration, which is capable of provisioning a Windows virtual machine and (among others) a dedicated network security group.

3. Terraform Configuration for a Windows 10 Virtual Machine and a proper Network Security Group

3.1 The Terraform Configuration Files

The full example is available at following link of my GitHub repository:

https://github.com/patkoch/iac_terraform_azure/tree/main/vm/win10-sg

Either clone GitHub repository, or copy the code and save it to *.tf files on your client. The content of each file can be seen below.

The example is distributed to three files:

• providers.tf
• main.tf
• variables.tf

providers.tf

terraform {
required_providers {
azurerm = {
source  = "hashicorp/azurerm"
version = "~> 2.65"
}

random = {
source  = "hashicorp/random"
version = "3.1.0"
}
}

required_version = ">= 0.14.9"
}

provider "azurerm" {
features {}
}


main.tf

resource "azurerm_resource_group" "rg" {
name     = "iac-azure-terraform"
location = "westeurope"
}

resource "azurerm_availability_set" "myavailabilityset" {
name                = "example-aset"
location            = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_virtual_network" "vnet" {
name                = "vNet"
location            = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "subnet" {
name                 = "internal"
resource_group_name  = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
}

resource "azurerm_public_ip" "my-public-ip" {
name                = "my-public-ip"
resource_group_name = azurerm_resource_group.rg.name
location            = azurerm_resource_group.rg.location
allocation_method   = "Dynamic"

tags = {
environment = "Testing"
}
}

resource "azurerm_network_interface" "mynetworkinterface" {
name                = "my-network-interface"
location            = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

ip_configuration {
name                          = "internal"
subnet_id                     = azurerm_subnet.subnet.id

}
}

# Windows 10 Virtual Machine
resource "azurerm_windows_virtual_machine" "myvirtualmachine" {
name                = "windows10-20h1"
resource_group_name = azurerm_resource_group.rg.name
location            = azurerm_resource_group.rg.location
size                = var.my_virtual_machine_size
availability_set_id = azurerm_availability_set.myavailabilityset.id
network_interface_ids = [
azurerm_network_interface.mynetworkinterface.id,
]

os_disk {
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "MicrosoftWindowsDesktop"
offer     = "windows-10"
sku       = "20h1-pro"
version   = "latest"
}
}

# Security Group for allowing RDP Connection
resource "azurerm_network_security_group" "sg-rdp-connection" {
name                = "allowrdpconnection"
location            = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

security_rule {
name                       = "rdpport"
priority                   = 100
direction                  = "Inbound"
access                     = "Allow"
protocol                   = "Tcp"
source_port_range          = "*"
destination_port_range     = "3389"
}

tags = {
environment = "Testing"
}
}

# Associate security group with network interface
resource "azurerm_network_interface_security_group_association" "example" {
network_interface_id      = azurerm_network_interface.mynetworkinterface.id
network_security_group_id = azurerm_network_security_group.sg-rdp-connection.id
}


variables.tf

variable "my_virtual_machine_password" {
default     = "P@$$w0rd1234!" description = "Password of the Virtual Machine" } variable "my_virtual_machine_size" { default = "Standard_D2_v4" description = "Size of the Virtual Machine" }  Applying this configuration creates a Windows virtual machine in Azure. Let’s have a closer look at the network security group in the next section. 3.2 The Network Security Group This network security group named “allowrdpconnection” contains a security rule “rdpport”, which defines an inbound rule for allowing the access to port 3389 using the Tcp protocol. That’s mandatory for making it possible to establish a RDP connection to the virtual machine after provisioning it. The priority has a value of 100 - the number has to be unique for each rule. Possible values are: >= 100 and <= 4096. A rule with the priorty 200 would be more important than a rule with a priority of 300. resource "azurerm_network_security_group" "sg-rdp-connection" { name = "allowrdpconnection" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name security_rule { name = "rdpport" priority = 100 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "3389" source_address_prefix = "*" destination_address_prefix = "*" } tags = { environment = "Testing" } }  The network security group has to be associated with the network interface - this is done with following block: resource "azurerm_network_interface_security_group_association" "example" { network_interface_id = azurerm_network_interface.mynetworkinterface.id network_security_group_id = azurerm_network_security_group.sg-rdp-connection.id }  3.3 Provisioning the Virtual Machine with Terraform If you’ve cloned my repository, then switch to the directory “iac_terraform_azure/vm/win10-sg”. Run following Terraform commands for creating the Windows virtual machine in Azure: 3.3.1 Terraform Init This conducts a connection to the backend and downloads the plugin. terraform init  3.3.2 Terraform Validate Verify the configuration by entering following command: terraform validate  3.3.2 Terraform Apply Start the provisioning by using this command: terraform apply -auto-approve  3.4 Verification of the Security Rule, of the common Prerequisites and of the Connection The virtual machine named “windows10-20h1” was created as a result: 3.4.1 The Security Rule Click at “windows10-20h1” and afterwards at “Networking” to prove, that the security rule was created: According to the policy, opening port 3389 outside of the virtual network should be done for testing only - this is indicated by the “warning” icon of the security rule. 3.4.2 The common Prerequisites Click at “connect” to let Azure verify some common prerequisites: The result should be a successfull verification: 3.4.3 The Connection with your IP Address as Source In addition, you can conduct a test with your ip address as connection source: Let’s recap the content of this section. It was about: • the structure of the Terraform configuration files • a more detailed view of the network security group • how to provision the virtual machine using Terraform commands • how to conduct some checks of the created virtual machine 4. Example of bad Port Prerequisites for a Windows Virtual Machine This section is about to view things from a different perspective: how can you recognize bad port prerequisites? 4.1 The Terraform Configuration File I’d like to demonstrate that with an example. The Terraform configuration file below is again capable for provisioning a Windows 10 virtual machine, including a network security group - but will cause some warnings. This configuration is valid and provisions a virtual machine, but it should be used just for demonstration only, as (among others) the network security group is not well defined with regard to the port prerequistes. ## Use this configuration just for testing purposes ## Resource Group resource "azurerm_resource_group" "rg" { name = "iac-azure-terraform" location = "westeurope" } ## Availability Set resource "azurerm_availability_set" "DemoAset" { name = "example-aset" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name } ## Virtual Network resource "azurerm_virtual_network" "vnet" { name = "vNet" address_space = ["10.0.0.0/16"] location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name } ## Subnet resource "azurerm_subnet" "subnet" { name = "internal" resource_group_name = azurerm_resource_group.rg.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefixes = ["10.0.2.0/24"] } ## Public IP resource "azurerm_public_ip" "example" { name = "acceptanceTestPublicIp1" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location allocation_method = "Dynamic" tags = { environment = "Production" } } ## Network Interface resource "azurerm_network_interface" "example" { name = "example-nic" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name ip_configuration { name = "internal" subnet_id = azurerm_subnet.subnet.id private_ip_address_allocation = "Dynamic" public_ip_address_id = azurerm_public_ip.example.id } } ## Virtual Machine resource "azurerm_windows_virtual_machine" "example" { name = "windows10-20h1" resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location size = "Standard_D2_v4" admin_username = "adminuser" admin_password = "P@$$w0rd1234!"
availability_set_id = azurerm_availability_set.DemoAset.id
network_interface_ids = [
azurerm_network_interface.example.id,
]

os_disk {
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "MicrosoftWindowsDesktop"
offer     = "windows-10"
sku       = "20h1-pro"
version   = "latest"
}
}

## Network Security Group
resource "azurerm_network_security_group" "example" {
name                = "acceptanceTestSecurityGroup1"
location            = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

security_rule {
name                       = "test123"
priority                   = 100
direction                  = "Inbound"
access                     = "Allow"
protocol                   = "Tcp"
source_port_range          = "*"
destination_port_range     = "*"
}
}


4.2 The Warnings and how to fix resolve them

Following warning can be recognized after a complete creation of the virtual machine:

It refers to a non existing security rule for port 3389 - as port 3389 is not explicitly mentioned. I’m going to fix that by adapting the security rule:

security_rule {
name                       = "test123"
priority                   = 100
direction                  = "Inbound"
access                     = "Allow"
protocol                   = "Tcp"
source_port_range          = "*"
destination_port_range     = "3389"
}


Now, a dedicated security rule for this port was added - but an additional prerequisite is missing. A next warning appears:

This means that I also need to associate the security group with the network interface. Following block would correct that:

resource "azurerm_network_interface_security_group_association" "example" {
network_interface_id      = azurerm_network_interface.example.id
network_security_group_id = azurerm_network_security_group.example.id
}


Again, let’s start some checks…

…which seem to fit.

Finally, the RDP file can be downloaded and a connection can be established.

Conclusion

Terraform configuration templates for virtual machines can be found easily. If you’d like to use the virtual machines in production, then verify and ensure the security related aspects. Pay attention to the hints in Azure and take care - it’s worth to invest that time.

References

github.com/patkoch - win10-sg

learn.microsoft.com - network-security-groups-overview

registry.terraform.io - network_security_group

learn.microsoft.com - virtual-networks-overview

learn.microsoft.com - virtual-network-for-azure-services