Network Security Groups - Microsegmentation in Azure
Introduction
Network Security Groups (NSGs) provide stateful packet filtering to control inbound and outbound traffic for Azure resources. NSGs can be associated with subnets or individual network interfaces, enabling fine-grained security policies and microsegmentation within your virtual networks.
In this post, we will explore how to design and implement effective NSG configurations.
NSG Rule Basics
NSG rules are evaluated by priority (100-4096, lower = higher priority):
- Direction: Inbound or Outbound
- Priority: 100-4096 (lower numbers processed first)
- Source/Destination: IP, CIDR, service tag, or ASG
- Protocol: TCP, UDP, ICMP, or Any
- Port Range: Single port, range, or * for all
- Action: Allow or Deny
Creating NSGs with Terraform
Comprehensive NSG configuration:
# Web Tier NSG
resource "azurerm_network_security_group" "web" {
name = "nsg-web-tier"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
# Allow HTTPS from Internet
security_rule {
name = "allow-https-inbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
# Allow HTTP from Internet (redirect to HTTPS)
security_rule {
name = "allow-http-inbound"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
# Allow Azure Load Balancer health probes
security_rule {
name = "allow-lb-probe"
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
}
# Allow outbound to API tier
security_rule {
name = "allow-to-api-tier"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_address_prefix = "*"
destination_address_prefix = "10.0.2.0/24"
}
tags = {
Environment = "Production"
Tier = "Web"
}
}
# API Tier NSG
resource "azurerm_network_security_group" "api" {
name = "nsg-api-tier"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
# Allow from Web Tier only
security_rule {
name = "allow-from-web-tier"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_address_prefix = "10.0.1.0/24"
destination_address_prefix = "*"
}
# Allow health checks from Load Balancer
security_rule {
name = "allow-lb-health"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
}
# Deny direct internet access
security_rule {
name = "deny-internet-inbound"
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
# Allow outbound to SQL
security_rule {
name = "allow-to-sql"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_address_prefix = "*"
destination_address_prefix = "10.0.3.0/24"
}
tags = {
Environment = "Production"
Tier = "API"
}
}
# Data Tier NSG
resource "azurerm_network_security_group" "data" {
name = "nsg-data-tier"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
# Allow SQL from API Tier
security_rule {
name = "allow-sql-from-api"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_address_prefix = "10.0.2.0/24"
destination_address_prefix = "*"
}
# Deny all other inbound
security_rule {
name = "deny-all-inbound"
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Deny internet outbound
security_rule {
name = "deny-internet-outbound"
priority = 4000
direction = "Outbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "Internet"
}
tags = {
Environment = "Production"
Tier = "Data"
}
}
# Associate NSGs with subnets
resource "azurerm_subnet_network_security_group_association" "web" {
subnet_id = azurerm_subnet.web.id
network_security_group_id = azurerm_network_security_group.web.id
}
resource "azurerm_subnet_network_security_group_association" "api" {
subnet_id = azurerm_subnet.api.id
network_security_group_id = azurerm_network_security_group.api.id
}
resource "azurerm_subnet_network_security_group_association" "data" {
subnet_id = azurerm_subnet.data.id
network_security_group_id = azurerm_network_security_group.data.id
}
Using Service Tags
Leverage Azure service tags for dynamic rule updates:
resource "azurerm_network_security_group" "managed_services" {
name = "nsg-managed-services"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
# Allow Azure Backup
security_rule {
name = "allow-azure-backup"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureBackup"
}
# Allow Azure Storage
security_rule {
name = "allow-azure-storage"
priority = 110
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "Storage.EastUS"
}
# Allow Azure Monitor
security_rule {
name = "allow-azure-monitor"
priority = 120
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureMonitor"
}
# Allow Azure Key Vault
security_rule {
name = "allow-keyvault"
priority = 130
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureKeyVault"
}
# Allow Azure Active Directory
security_rule {
name = "allow-aad"
priority = 140
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureActiveDirectory"
}
# Allow Azure SQL
security_rule {
name = "allow-azure-sql"
priority = 150
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["1433", "11000-11999"]
source_address_prefix = "*"
destination_address_prefix = "Sql.EastUS"
}
}
NSG Flow Logs
Enable NSG flow logs for traffic analysis:
from azure.mgmt.network import NetworkManagementClient
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
network_client = NetworkManagementClient(credential, subscription_id)
def enable_nsg_flow_logs(nsg_resource_group, nsg_name, storage_account_id, workspace_id):
"""Enable NSG flow logs with traffic analytics."""
flow_log = network_client.flow_logs.begin_create_or_update(
resource_group_name="NetworkWatcherRG",
network_watcher_name="NetworkWatcher_eastus",
flow_log_name=f"flowlog-{nsg_name}",
parameters={
"location": "eastus",
"properties": {
"targetResourceId": f"/subscriptions/{subscription_id}/resourceGroups/{nsg_resource_group}/providers/Microsoft.Network/networkSecurityGroups/{nsg_name}",
"storageId": storage_account_id,
"enabled": True,
"retentionPolicy": {
"days": 30,
"enabled": True
},
"format": {
"type": "JSON",
"version": 2
},
"flowAnalyticsConfiguration": {
"networkWatcherFlowAnalyticsConfiguration": {
"enabled": True,
"workspaceId": workspace_id,
"workspaceRegion": "eastus",
"trafficAnalyticsInterval": 10
}
}
}
}
).result()
return flow_log
# Enable for all NSGs
storage_id = f"/subscriptions/{subscription_id}/resourceGroups/rg-monitoring/providers/Microsoft.Storage/storageAccounts/flowlogsstorage"
workspace_id = "log-analytics-workspace-guid"
for nsg in ["nsg-web-tier", "nsg-api-tier", "nsg-data-tier"]:
enable_nsg_flow_logs("rg-networking", nsg, storage_id, workspace_id)
print(f"Enabled flow logs for {nsg}")
Diagnosing NSG Issues
Troubleshoot connectivity problems:
def diagnose_nsg_rules(resource_group, vm_name, direction, remote_ip, remote_port, protocol="TCP"):
"""Check effective NSG rules for a VM."""
# Get effective security rules
effective_rules = network_client.network_interfaces.list_effective_network_security_groups(
resource_group_name=resource_group,
network_interface_name=f"{vm_name}-nic"
).result()
print(f"Effective rules for {vm_name} ({direction}):")
for nsg in effective_rules.value:
print(f"\n NSG: {nsg.network_security_group.id.split('/')[-1]}")
rules = nsg.effective_security_rules if direction == "Inbound" else nsg.effective_security_rules
for rule in sorted(rules, key=lambda x: x.priority):
if rule.direction.lower() == direction.lower():
print(f" [{rule.priority}] {rule.name}: {rule.access}")
print(f" Source: {rule.source_address_prefix or rule.source_address_prefixes}")
print(f" Dest: {rule.destination_address_prefix or rule.destination_address_prefixes}:{rule.destination_port_range or rule.destination_port_ranges}")
# Use IP flow verify to test specific connection
result = network_client.network_watchers.begin_verify_ip_flow(
resource_group_name="NetworkWatcherRG",
network_watcher_name="NetworkWatcher_eastus",
parameters={
"targetResourceId": f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{vm_name}",
"direction": direction,
"protocol": protocol,
"localPort": "*",
"remotePort": str(remote_port),
"localIPAddress": "10.0.1.4", # VM IP
"remoteIPAddress": remote_ip
}
).result()
print(f"\nIP Flow Verify: {result.access}")
if result.rule_name:
print(f" Matching Rule: {result.rule_name}")
return result
# Test connectivity
diagnose_nsg_rules("rg-compute", "vm-web-01", "Inbound", "0.0.0.0", 443)
Managing NSGs at Scale
Automate NSG management across resources:
def audit_nsg_rules(subscription_id, required_deny_rules):
"""Audit all NSGs for compliance with security policies."""
nsgs = network_client.network_security_groups.list_all()
non_compliant = []
for nsg in nsgs:
has_required_rules = True
for required_rule in required_deny_rules:
found = False
for rule in nsg.security_rules:
if (rule.direction == required_rule["direction"] and
rule.access == "Deny" and
rule.destination_port_range == required_rule["port"]):
found = True
break
if not found:
has_required_rules = False
non_compliant.append({
"nsg": nsg.name,
"resource_group": nsg.id.split("/")[4],
"missing_rule": required_rule
})
return non_compliant
# Check for required deny rules
required_rules = [
{"direction": "Inbound", "port": "22"}, # SSH should be restricted
{"direction": "Inbound", "port": "3389"}, # RDP should be restricted
]
issues = audit_nsg_rules(subscription_id, required_rules)
for issue in issues:
print(f"NSG {issue['nsg']} missing rule for port {issue['missing_rule']['port']}")
Conclusion
Network Security Groups are fundamental to Azure network security, providing stateful packet filtering at the subnet and NIC level. Effective NSG design follows the principle of least privilege, using explicit deny rules and service tags to maintain security while allowing necessary traffic.
Enable NSG flow logs and traffic analytics to gain visibility into your network traffic patterns and quickly identify security issues. Regular auditing of NSG rules ensures compliance with security policies and helps identify overly permissive configurations.