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
- 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"
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
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 {
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:
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:
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
learn.microsoft.com - network-security-groups-overview
registry.terraform.io - network_security_group