Skip to main content

Network Security Groups (NSG)

What is a Network Security Group?

A Network Security Group (NSG) is a fundamental Azure security feature that acts as a stateful firewall to filter network traffic to and from Azure resources in a virtual network. Think of it as a software-defined firewall that controls inbound and outbound traffic using security rules.

Key Capabilities:

  • 5-Tuple Filtering: Source IP, source port, destination IP, destination port, protocol
  • Stateful Inspection: Return traffic is automatically allowed
  • Priority-Based Rules: Rules processed from lowest to highest priority (100-4096)
  • Default Rules: Built-in rules that cannot be deleted (priority 65000+)
  • Flexible Scope: Apply to subnets, individual NICs, or both

How NSGs Work

Rule Processing

NSGs evaluate traffic using security rules in priority order:

  1. Lowest priority number processed first (100 before 200)
  2. First matching rule wins - no further rules are evaluated
  3. Default rules applied last if no custom rules match
  4. Deny all inbound by default, allow all outbound by default

Default Rules (Cannot be Deleted)

Inbound:

  • Allow VNet traffic (priority 65000)
  • Allow Azure Load Balancer (priority 65001)
  • Deny all other inbound (priority 65500)

Outbound:

  • Allow VNet traffic (priority 65000)
  • Allow internet traffic (priority 65001)
  • Deny all other outbound (priority 65500)

How to Use NSGs

Create an NSG

Using Azure CLI

# Create NSG
az network nsg create \
--resource-group rg-networking \
--name nsg-web-tier \
--location eastus

# Add inbound rule to allow HTTPS
az network nsg rule create \
--resource-group rg-networking \
--nsg-name nsg-web-tier \
--name Allow-HTTPS-Inbound \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes Internet \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 443

# Add inbound rule to allow HTTP
az network nsg rule create \
--resource-group rg-networking \
--nsg-name nsg-web-tier \
--name Allow-HTTP-Inbound \
--priority 110 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes Internet \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 80

# Deny all other inbound (explicit, though default denies anyway)
az network nsg rule create \
--resource-group rg-networking \
--nsg-name nsg-web-tier \
--name Deny-All-Inbound \
--priority 4096 \
--direction Inbound \
--access Deny \
--protocol '*' \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges '*'

Associate NSG to Subnet

# Associate NSG to subnet
az network vnet subnet update \
--resource-group rg-networking \
--vnet-name vnet-spoke-prod \
--name snet-web \
--network-security-group nsg-web-tier

Associate NSG to NIC

# Associate NSG to network interface
az network nic update \
--resource-group rg-compute \
--name nic-vm-web-01 \
--network-security-group nsg-web-tier

Terraform Example

# Network Security Group for Web Tier
resource "azurerm_network_security_group" "web" {
name = "nsg-web-tier"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location

tags = {
Environment = "Production"
Tier = "Web"
}
}

# Allow HTTPS from Internet
resource "azurerm_network_security_rule" "allow_https" {
name = "Allow-HTTPS-Inbound"
resource_group_name = azurerm_resource_group.networking.name
network_security_group_name = azurerm_network_security_group.web.name
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
resource "azurerm_network_security_rule" "allow_http" {
name = "Allow-HTTP-Inbound"
resource_group_name = azurerm_resource_group.networking.name
network_security_group_name = azurerm_network_security_group.web.name
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}

# Data Tier NSG - more restrictive
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 App Tier only
resource "azurerm_network_security_rule" "allow_sql_from_app" {
name = "Allow-SQL-From-App-Tier"
resource_group_name = azurerm_resource_group.networking.name
network_security_group_name = azurerm_network_security_group.data.name
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_address_prefix = "10.1.2.0/24" # App subnet CIDR
destination_address_prefix = "*"
}

# Deny all inbound from Internet
resource "azurerm_network_security_rule" "deny_internet_inbound" {
name = "Deny-Internet-Inbound"
resource_group_name = azurerm_resource_group.networking.name
network_security_group_name = azurerm_network_security_group.data.name
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}

# Associate NSG to Subnet
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" "data" {
subnet_id = azurerm_subnet.data.id
network_security_group_id = azurerm_network_security_group.data.id
}

Application Security Groups (ASG)

Application Security Groups simplify NSG rule management by grouping VMs logically instead of by IP addresses.

Terraform Example with ASG

# Define Application Security Groups
resource "azurerm_application_security_group" "web_servers" {
name = "asg-web-servers"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
}

resource "azurerm_application_security_group" "database_servers" {
name = "asg-database-servers"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
}

# NSG Rule using ASG
resource "azurerm_network_security_rule" "web_to_db" {
name = "Allow-Web-To-Database"
resource_group_name = azurerm_resource_group.networking.name
network_security_group_name = azurerm_network_security_group.data.name
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_application_security_group_ids = [azurerm_application_security_group.web_servers.id]
destination_application_security_group_ids = [azurerm_application_security_group.database_servers.id]
}

# Associate ASG to NIC
resource "azurerm_network_interface_application_security_group_association" "web_vm" {
network_interface_id = azurerm_network_interface.web_vm.id
application_security_group_id = azurerm_application_security_group.web_servers.id
}

CI/CD Integration

Validate NSG Rules in Pipeline

name: NSG Security Validation

on:
pull_request:
paths:
- '**/*.tf'
- 'networking/**'

jobs:
validate-nsg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Check for overly permissive rules
run: |
# Check for 0.0.0.0/0 or * on sensitive ports
if grep -r "source_address_prefix.*=.*\"\*\"" terraform/ | grep -E "22|3389|1433|3306"; then
echo "ERROR: Found overly permissive NSG rule allowing any source to sensitive port"
exit 1
fi

- name: Verify no direct internet access to data tier
run: |
# Ensure data tier NSGs don't allow Internet source
if grep -A 10 "nsg-data" terraform/*.tf | grep -i 'source_address_prefix.*Internet'; then
echo "ERROR: Data tier should not allow traffic from Internet"
exit 1
fi

- name: Check NSG rule priorities
run: |
# Ensure explicit deny rules have higher priority than allows
python scripts/validate_nsg_priorities.py

Azure DevOps Pipeline

trigger:
branches:
include:
- main
paths:
include:
- networking/**

pool:
vmImage: 'ubuntu-latest'

steps:
- task: AzureCLI@2
displayName: 'Deploy NSG Rules'
inputs:
azureSubscription: 'Production'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Deploy NSG
az deployment group create \
--resource-group rg-networking \
--template-file nsg-template.bicep \
--parameters @nsg-parameters.json

# Enable NSG Flow Logs
az network watcher flow-log create \
--resource-group rg-networking \
--name nsg-web-flow-log \
--nsg nsg-web-tier \
--storage-account stlogs \
--enabled true \
--retention 90

Best Practices

1. Implement Defense in Depth

Apply NSGs at Both Subnet and NIC Levels

  • Subnet NSG: Broad security boundary
  • NIC NSG: Granular per-VM security

Don't: Rely on subnet-level NSG alone for critical workloads

2. Use Least Privilege Principle

Do:

# Specific source and destination
az network nsg rule create \
--priority 100 \
--source-address-prefixes 10.1.1.0/24 \
--destination-port-ranges 443

Don't:

# Overly broad
az network nsg rule create \
--priority 100 \
--source-address-prefixes '*' \
--destination-port-ranges '*'

3. Deny All Inbound by Default

Explicitly allow only required traffic:

  • Priority 100-3999: Allow rules for specific services
  • Priority 4000: Deny all remaining inbound

4. Use Service Tags

Service tags represent groups of IP addresses for Azure services:

resource "azurerm_network_security_rule" "allow_azurecloud" {
name = "Allow-AzureCloud"
priority = 200
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureCloud" # Service Tag
# ...
}

Common Service Tags:

  • Internet
  • VirtualNetwork
  • AzureLoadBalancer
  • AzureCloud
  • Storage, Sql, AzureKeyVault
  • AzureMonitor

5. Enable NSG Flow Logs

Flow logs track allowed and denied traffic for security analysis:

# Create Log Analytics workspace
az monitor log-analytics workspace create \
--resource-group rg-monitoring \
--workspace-name law-nsg-logs

# Enable flow logs
az network watcher flow-log create \
--name nsg-web-flow-log \
--nsg nsg-web-tier \
--resource-group rg-networking \
--storage-account stlogs \
--workspace /subscriptions/{sub}/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/law-nsg-logs \
--enabled true \
--retention 90 \
--traffic-analytics true

6. Use Application Security Groups for Complex Scenarios

Use ASGs when:

  • Many VMs with similar security requirements
  • VMs dynamically scale (autoscale sets)
  • IP addresses change frequently

Avoid ASGs when:

  • Simple, static environments
  • Single VM scenarios

7. Document NSG Rules

Use descriptive rule names and descriptions:

resource "azurerm_network_security_rule" "example" {
name = "Allow-HTTPS-From-AppGW" # Descriptive
description = "Allow HTTPS from Application Gateway subnet to web tier"
# ...
}

Common Patterns

Three-Tier Application

Web Tier NSG:
- Priority 100: Allow 443 from Internet
- Priority 110: Allow 80 from Internet
- Priority 4000: Deny all inbound

App Tier NSG:
- Priority 100: Allow 8080 from Web subnet (10.1.1.0/24)
- Priority 4000: Deny all inbound

Data Tier NSG:
- Priority 100: Allow 1433 from App subnet (10.1.2.0/24)
- Priority 200: Deny Internet inbound
- Priority 4000: Deny all inbound

Management Access

# Use Azure Bastion instead of allowing RDP/SSH from Internet
# If you must allow, use specific IP and JIT access

az network nsg rule create \
--name Allow-RDP-From-Admin-IP \
--priority 100 \
--source-address-prefixes 203.0.113.50/32 \
--destination-port-ranges 3389 \
--access Allow \
# ... (Only as temporary measure - prefer Bastion)

Things to Avoid

Don't allow 0.0.0.0/0 (any source) to RDP/SSH (ports 22, 3389) ❌ Don't allow wildcard () source to sensitive ports (1433, 3306, 5432, 27017) ❌ Don't expose databases directly to Internet ❌ Don't forget to enable NSG flow logs for security analysis ❌ Don't use overlapping priority numbers (hard to manage) ❌ Don't create hundreds of rules (use ASG instead) ❌ Don't allow all ports () unless absolutely necessary ❌ Don't apply NSG to GatewaySubnet or AzureFirewallSubnet ❌ Don't forget to test NSG changes in non-production first

Do use Azure Bastion for management access ✅ Do implement least privilege access ✅ Do use service tags for Azure services ✅ Do enable diagnostic logging ✅ Do review NSG rules quarterly ✅ Do use descriptive naming ✅ Do implement deny rules explicitly for critical resources ✅ Do use Application Security Groups for dynamic environments

Monitoring & Troubleshooting

View Effective Security Rules

# See all rules affecting a NIC (combined subnet + NIC NSGs)
az network nic list-effective-nsg \
--resource-group rg-compute \
--name nic-vm-web-01 \
-o table

Test Connectivity

# Use Network Watcher IP Flow Verify
az network watcher test-ip-flow \
--resource-group rg-compute \
--vm vm-web-01 \
--direction Inbound \
--protocol TCP \
--local 10.1.1.5:443 \
--remote 0.0.0.0:12345

Query Flow Logs with KQL

AzureNetworkAnalytics_CL
| where SubType_s == "FlowLog"
| where FlowStatus_s == "D" // Denied traffic
| summarize count() by DestPort_d, SrcIP_s
| order by count_ desc
| take 20

Security Recommendations

  1. Never allow unrestricted inbound from Internet (0.0.0.0/0) on management ports
  2. Use Azure Firewall or NVA for centralized logging and inspection
  3. Implement network segmentation with NSGs on every subnet
  4. Enable Azure DDoS Protection for internet-facing applications
  5. Use Private Endpoints to eliminate public endpoints for PaaS services
  6. Regularly audit NSG rules for overly permissive access
  7. Automate NSG deployments via IaC (Terraform, Bicep)