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     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

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

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"
  address_space       = ["10.0.0.0/16"]
  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
  address_prefixes     = ["10.0.2.0/24"]
}

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
    private_ip_address_allocation = "Dynamic"

    public_ip_address_id = azurerm_public_ip.my-public-ip.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
  admin_username      = "adminuser"
  admin_password      = var.my_virtual_machine_password
  availability_set_id = azurerm_availability_set.myavailabilityset.id
  network_interface_ids = [
    azurerm_network_interface.mynetworkinterface.id,
  ]

  os_disk {
    caching              = "ReadWrite"
    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"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  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 
terraform_init

3.3.2 Terraform Validate

Verify the configuration by entering following command:

terraform validate
terraform_validate

3.3.2 Terraform Apply

Start the provisioning by using this command:

terraform apply -auto-approve
terraform_apply

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:

azure_portal_virtual_machine_overview

3.4.1 The Security Rule

Click at “windows10-20h1” and afterwards at “Networking” to prove, that the security rule was created:

azure_portal_vm_network

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.

azure_portal_warning_rdp_port.png

3.4.2 The common Prerequisites

Click at “connect” to let Azure verify some common prerequisites:

Verify_PortPrerequisites.png

The result should be a successfull verification:

Connect-RDP-Successful-Prerequisites

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:

Test-Connection.png

Let’s recap the content of this section.

It was about:

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 {
    caching              = "ReadWrite"
    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     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

4.2 The Warnings and how to fix resolve them

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

azure_portal_rdp_connection_port_prereq_not_met

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"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

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

azure_portal_security_group_not_associated

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…

VeriyConnection-InProgress

…which seem to fit.

VerifyConnectionSettings

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