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:
- Lowest priority number processed first (100 before 200)
- First matching rule wins - no further rules are evaluated
- Default rules applied last if no custom rules match
- 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:
InternetVirtualNetworkAzureLoadBalancerAzureCloudStorage,Sql,AzureKeyVaultAzureMonitor
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
- Never allow unrestricted inbound from Internet (0.0.0.0/0) on management ports
- Use Azure Firewall or NVA for centralized logging and inspection
- Implement network segmentation with NSGs on every subnet
- Enable Azure DDoS Protection for internet-facing applications
- Use Private Endpoints to eliminate public endpoints for PaaS services
- Regularly audit NSG rules for overly permissive access
- Automate NSG deployments via IaC (Terraform, Bicep)