Mastering Azure Network Security Groups with Terraform Infrastructure as Code

Managing network security in a cloud environment without a version-controlled system often leads to "rule creep," where Network Security Groups (NSGs) become cluttered with legacy rules that no one remembers adding and no one dares to remove. By implementing Infrastructure as Code (IaC) via Terraform, security rules become auditable, reviewable, and reproducible. This approach ensures that your security posture is documented in code rather than hidden in a web portal, allowing for seamless peer reviews and rapid disaster recovery.

Azure Network Security Groups act as a virtual firewall for your cloud resources, allowing you to filter network traffic to and from Azure resources in an Azure virtual network (VNet). When combined with Terraform, you can programmatically define inbound and outbound security rules, associate them with specific subnets or network interfaces, and maintain a strict security perimeter across different tiers of your application.

Core Infrastructure Prerequisites and Provider Configuration

Before deploying an NSG, the environment must be properly configured to allow Terraform to communicate with the Azure API. For modern deployments, Terraform 1.0 or later is required, though certain specialized modules may require version 1.10.0 or higher.

The authentication process typically relies on the Azure CLI. Once authenticated, the provider configuration must specify the azurerm provider and the associated subscription ID to ensure resources are provisioned in the correct tenant.

The following configuration establishes the necessary provider block for an Azure environment:

```hcl
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}

provider "azurerm" {
features {}
subscriptionid = var.subscriptionid
}

variable "subscription_id" {
description = "Azure subscription ID where the resources will be managed"
type = string
}
```

When managing networking resources, it is common practice to either create a new resource group or reference an existing one using a data block. Referencing an existing group prevents the accidental deletion of shared networking infrastructure.

```hcl

Reference an existing resource group for networking

data "azurermresourcegroup" "networking" {
name = "rg-networking-prod-eus"
}
```

Architectural Approaches to NSG Rule Definition

Terraform provides two primary methods for defining security rules within an NSG: inline blocks and standalone resources. Choosing between these depends on the complexity of the security requirements and the need for modularity.

Inline Security Rules

Inline rules are defined directly within the azurerm_network_security_group resource block. This method is ideal for simpler configurations where the rules are static and closely tied to the group's lifecycle.

For instance, an application tier NSG might require a specific rule to allow traffic from a web tier on port 8080 and another to allow health check probes from the Azure Load Balancer.

```hcl
resource "azurermnetworksecuritygroup" "app" {
name = "nsg-app-prod"
location = data.azurerm
resourcegroup.networking.location
resource
groupname = data.azurermresource_group.networking.name

# Allow traffic from web tier on port 8080
securityrule {
name = "Allow-Web-To-App"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source
portrange = "*"
destination
portrange = "8080"
source
addressprefix = "10.0.1.0/24"
destination
address_prefix = "*"
}

# Allow health check probes from Azure Load Balancer
securityrule {
name = "Allow-LB-Probes"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source
portrange = "*"
destination
portrange = "80"
source
addressprefix = "AzureLoadBalancer"
destination
address_prefix = "*"
}
}
```

Standalone Security Rule Resources

For complex environments, using the azurerm_network_security_rule resource is preferred. This decouples the rule from the NSG resource, allowing rules to be added, removed, or modified without risking the replacement of the entire NSG.

This is particularly useful for implementing "Deny All" rules at the end of a priority chain to ensure a Zero Trust architecture.

```hcl

Create a basic NSG without inline rules

resource "azurermnetworksecuritygroup" "web" {
name = "nsg-web-prod"
location = data.azurerm
resourcegroup.networking.location
resource
groupname = data.azurermresource_group.networking.name
tags = {
Environment = "production"
Tier = "web"
ManagedBy = "terraform"
}
}

Define a specific rule to allow SSH from a trusted subnet

resource "azurermnetworksecurityrule" "allowssh" {
name = "Allow-SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
sourceportrange = ""
destination_port_range = "22"
source_address_prefix = "10.0.255.0/24"
destination_address_prefix = "
"
resourcegroupname = data.azurermresourcegroup.networking.name
networksecuritygroupname = azurermnetworksecuritygroup.web.name
}

Explicitly deny all other inbound traffic

resource "azurermnetworksecurityrule" "denyallinbound" {
name = "Deny-All-Inbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source
portrange = "*"
destination
portrange = "*"
source
addressprefix = "*"
destination
addressprefix = "*"
resource
groupname = data.azurermresourcegroup.networking.name
network
securitygroupname = azurermnetworksecurity_group.web.name
}
```

Application Security Groups (ASGs) for Scalable Logic

A major limitation of standard NSGs is the reliance on IP addresses. In dynamic cloud environments where VMs are scaled up or down, maintaining lists of IP addresses in security rules is an administrative nightmare. Application Security Groups (ASGs) solve this by allowing you to group network interfaces based on their function (e.g., "WebServers" or "DBServers") and use that group as a source or destination in an NSG rule.

By referencing an ASG ID instead of a CIDR block, the rule remains constant even as the underlying virtual machines change.

Implementing ASGs with Terraform

First, define the ASGs within the resource group:

```hcl
resource "azurermresourcegroup" "example" {
name = "my-resources"
location = "West Europe"
}

resource "azurermapplicationsecuritygroup" "first" {
name = "asg-first"
location = "eastus"
resource
groupname = azurermresource_group.example.name
}

resource "azurermapplicationsecuritygroup" "second" {
name = "asg-second"
location = "eastus"
resource
groupname = azurermresource_group.example.name
}
```

These ASGs can then be utilized within an NSG module or resource to create logic-based rules:

Rule Name Source Destination Port Purpose
SSH-Access ASG First VM 22 Admin access from jumpbox ASG
App-Traffic ASG First ASG Second 8080 Web tier to App tier traffic

Modular Deployment and Predefined Patterns

For organizations managing hundreds of NSGs, writing raw resources becomes repetitive. Using Terraform modules allows for the standardization of security patterns. Modules can provide "predefined" rules (such as standard HTTP/S access) while allowing for "custom" rules via input variables.

Using a Specialized NSG Module

The following example demonstrates the usage of a high-level module to provision an NSG with a mix of predefined and custom logic:

```hcl
module "network-security-group" {
source = "Azure/network-security-group/azurerm"
resourcegroupname = azurermresourcegroup.example.name
location = "eastus"
securitygroupname = "nsg"

predefinedrules = [
{
name = "SSH"
priority = "500"
source
applicationsecuritygroupids = [azurermapplicationsecuritygroup.first.id]
}
]

customrules = [
{
name = "myhttp"
priority = "200"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
destination
portrange = "8080"
description = "description-myhttp"
destination
applicationsecuritygroupids = [azurermapplicationsecuritygroup.second.id]
}
]

tags = {
environment = "dev"
costcenter = "it"
}
}
```

Alternatively, some modules offer specific templates, such as an HTTP-optimized module, where only specific custom overrides (like SSH) need to be provided:

```hcl
module "network-security-group" {
source = "Azure/network-security-group/azurerm//examples/HTTP"
resourcegroupname = azurermresourcegroup.example.name
securitygroupname = "nsg"

customrules = [
{
name = "ssh"
priority = "200"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
destination
port_range = "22"
}
]
}
```

Full Stack Deployment: Integrating NSG with Virtual Machines

To understand the full lifecycle, an NSG must be associated with a Network Interface (NIC) or a subnet. A typical deployment includes a Resource Group, a Virtual Network, an NSG, a NIC, and the VM itself.

The basic file structure for such a project generally consists of:
- main.tf: Resource definitions for the VM, NSG, NIC, and RG.
- variables.tf: Declarations for input variables.
- terraform.tfvars: Actual values for VM names and locations.
- outputs.tf: Values to be printed after deployment, such as the VM's public IP address.

Example Resource Chain

```hcl
resource "azurermresourcegroup" "rg" {
name = var.resourcegroupname
location = var.location
}

resource "azurermnetworksecuritygroup" "nsg" {
name = "demo-nsg"
location = azurerm
resourcegroup.rg.location
resource
groupname = azurermresource_group.rg.name

securityrule {
name = "Allow-SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source
portrange = "*"
destination
portrange = "22"
source
addressprefix = "*"
destination
address_prefix = "*"
}
}
```

Monitoring and Troubleshooting Network Security

One of the primary challenges with NSGs is that they silently drop traffic when a rule denies it. This lack of feedback can lead to hours of troubleshooting connectivity issues. To mitigate this, production environments should implement VNet Flow Logs and Traffic Analytics.

Flow logs capture information about the IP traffic flowing through your network security groups. By sending these logs to a storage account and analyzing them via a Log Analytics Workspace, engineers can correlate failed connection alerts with actual flow data.

Configuring Flow Logs via Terraform

The following configuration enables flow logs with a 30-day retention policy and integrates Traffic Analytics for visual insights into traffic patterns.

```hcl
resource "azurermnetworkwatcherflowlog" "flowlog" {
name = "flowlog-vnet-main"
resourcegroupname = data.azurermresourcegroup.networking.name
targetresourceid = data.azurermvirtualnetwork.main.id
storageaccountid = azurermstorageaccount.flow_logs.id
enabled = true
version = 2

retention_policy {
enabled = true
days = 30
}

trafficanalytics {
enabled = true
workspace
id = azurermloganalyticsworkspace.networking.workspaceid
workspaceregion = azurermloganalyticsworkspace.networking.location
workspaceresourceid = azurermloganalyticsworkspace.networking.id
interval
in_minutes = 10
}
}
```

Implementation Best Practices Summary

When deploying NSGs with Terraform, adhering to a set of operational standards ensures the infrastructure remains maintainable.

Technical Specification Comparison

Feature Inline Rules Standalone Resources ASG-Based Rules
Complexity Low Medium Medium/High
Flexibility Low High Very High
Management Single Block Separate Resources Logical Grouping
Scalability Poor Good Excellent
Best Use Case Small, static setups Large, evolving rulesets Dynamic VM scaling

Operational Guidelines

  • Version Control: Always use stable release versions of modules and providers. Avoid using the master branch of community modules directly, as it may contain unstable changes.
  • Naming Conventions: Use clear, descriptive names for rules (e.g., Allow-Web-To-App rather than Rule1).
  • Priority Management: Leave gaps between rule priorities (e.g., use 100, 110, 120) to allow for the insertion of new rules without renumbering the entire list.
  • Zero Trust: Always implement an explicit "Deny All" rule at a high priority number (e.g., 4096) to ensure no unintended traffic enters the network.
  • Documentation: Use the description field within azurerm_network_security_rule to explain the business reason for the rule.

Conclusion

Implementing Azure Network Security Groups through Terraform transforms network security from a manual, error-prone task into a disciplined engineering process. By leveraging the combination of standalone rule resources for flexibility and Application Security Groups for scalability, architects can create a security perimeter that evolves alongside the application. The integration of VNet Flow Logs and Traffic Analytics provides the necessary visibility to move from "guessing" why a connection is failing to "knowing" exactly which rule is dropping the traffic. Whether deploying a simple virtual machine for a demo or a complex multi-tier production environment, using Infrastructure as Code ensures that every port opened and every IP allowed is documented, reviewed, and auditable.

Sources

  1. deploy-an-azure-vm-with-network-security-group-nsg-using-terraform-3p01
  2. how-to-create-azure-network-security-groups-in-terraform
  3. terraform-az-modules/terraform-azurerm-nsg
  4. Azure/terraform-azurerm-network-security-group

Related Posts