Building secure, scalable network infrastructure in Azure requires careful planning of Virtual Networks (VNets), subnets, and Network Security Groups (NSGs). In this post, we’ll explore how these components work together to create a defense-in-depth security architecture, then show you how to automate the entire deployment using PowerShell scripts.
TL;DR
Want to jump straight to the code? Check out the complete working PowerShell scripts with detailed documentation on GitHub:
Azure VNet & NSG Automation Repository
This repository includes:
- 5 production-ready PowerShell scripts for automated deployment
- Network architecture diagram showing VNet topology and NSG rules
- Step-by-step deployment guide with troubleshooting tips
- Customization examples for different environments
Quick start: Clone the repo, run ./Deploy-All.ps1, and deploy a secure multi-tier network in minutes.
Understanding Azure Network Security Architecture
Before diving into automation, let’s understand the key components and how they work together:
Virtual Networks (VNets)
A Virtual Network is your private network space in Azure. It provides:
- Isolation: Your own private address space (e.g., 10.0.0.0/16)
- Segmentation: Ability to divide the network into subnets for different workloads
- Control: Complete control over DNS settings, IP addresses, and routing
Subnets
Subnets allow you to segment your VNet into smaller networks for different application tiers:
- App Subnet: Hosts web servers and application tier (10.0.1.0/24)
- Data Subnet: Hosts database servers and data tier (10.0.2.0/24)
This segmentation enables you to apply different security policies to each tier.
Network Security Groups (NSGs)
NSGs act as virtual firewalls, controlling inbound and outbound traffic at the subnet or VM level. Each NSG contains security rules that:
- Allow or Deny traffic based on source, destination, port, and protocol
- Process in priority order (lower numbers = higher priority)
- Evaluate statefully (return traffic for allowed connections is automatically permitted)
Network Architecture Diagram
The following diagram illustrates our complete VNet architecture with NSG rules and traffic flows:
This architecture implements a classic three-tier security model:
- Internet Traffic Layer: Public HTTP/HTTPS traffic enters through controlled entry points
- Application Layer: App subnet accepts web traffic and communicates with the data layer
- Data Layer: Database subnet only accepts connections from the application layer
Understanding NSG Traffic Flow
The NSG rules create a defense-in-depth security posture with the principle of least privilege:
App Subnet Security Rules
Inbound Rules:
- ✅ Priority 100: Allow HTTP (80) from Internet
- ✅ Priority 110: Allow HTTPS (443) from Internet
- ❌ Priority 4096: Deny All other inbound traffic
Outbound Rules:
- ✅ Priority 100: Allow PostgreSQL (5432) to Data Subnet (10.0.2.0/24)
- ❌ Priority 4096: Deny All other outbound traffic
What this means: Web servers can receive public HTTP/HTTPS traffic and initiate database connections, but nothing else. This prevents lateral movement and limits exposure if a web server is compromised.
Data Subnet Security Rules
Inbound Rules:
- ✅ Priority 100: Allow PostgreSQL (5432) from App Subnet (10.0.1.0/24)
- ❌ Priority 4096: Deny All other inbound traffic
Outbound Rules:
- ❌ Priority 4096: Deny All outbound traffic
What this means: Database servers only accept connections from the application tier on the database port. All outbound traffic is blocked, preventing data exfiltration even if the database is compromised.
Traffic Flow Analysis
Inbound for App Subnet:
Traffic from the Internet is allowed on ports 80 and 443. Any other inbound attempts are rejected by the “DenyAllInbound” rule. This prevents unauthorized access and ensures that only web traffic reaches your application servers.
Outbound from App Subnet:
The App Subnet can initiate outbound connections to the Data Subnet on port 5432. Once this rule processes, a catch-all “DenyAllOutbound” rule blocks any other outbound traffic, thereby restricting unnecessary exposure.
Inbound for Data Subnet:
Only traffic from the App Subnet is permitted on the database port (5432), securing your sensitive data against any unsolicited inbound requests.
Outbound for Data Subnet:
By denying all outbound traffic, the Data Subnet ensures that even if an attacker gains access to the database servers, minimal data exfiltration or lateral movement can occur.
These layered security measures enforce the principle of least privilege, ensuring that every component of your application can only access what is absolutely required.
Automating the Deployment with PowerShell
Now that we understand the architecture, let’s automate the deployment. Our solution consists of several PowerShell scripts that work together:
| Script | Purpose |
|---|---|
01-Create-ResourceGroups.ps1 |
Creates resource groups in multiple Azure regions |
02-Create-VNet-Subnets.ps1 |
Provisions VNet with App and Data subnets |
03-Create-NSGs.ps1 |
Creates NSGs with security rules for each subnet |
04-Delete-ResourceGroups.ps1 |
Safely deletes resource groups with confirmation |
Deploy-All.ps1 |
Master script that runs all deployment steps in sequence |
Each script includes detailed logging with emojis for visual feedback, error handling, and follows Azure naming conventions.
Prerequisites
Before running these scripts, ensure you have:
- Azure CLI installed and authenticated (
az login) - PowerShell 7+ (PowerShell Core) for cross-platform compatibility
- Appropriate Azure permissions to create resource groups, VNets, and NSGs
- Basic understanding of Azure networking concepts
Step 1: Creating Resource Groups
The first script (01-Create-ResourceGroups.ps1) creates resource groups in multiple Azure regions. It includes error handling and visual feedback with emojis.
# Define resource groups and their regions
$resourceGroups = @{
"rg-network-eastus" = "eastus"
"rg-network-westus" = "westus"
}
Write-Host -ForegroundColor Cyan "🌍 Azure Resource Group Creation Script"
# Create each resource group in the specified region
foreach ($resourceGroup in $resourceGroups.Keys) {
$region = $resourceGroups[$resourceGroup]
Write-Host -ForegroundColor Blue "🚀 Creating resource group '$resourceGroup' in region '$region'..."
try {
az group create --name $resourceGroup --location $region --output jsonc
Write-Host -ForegroundColor Green "✅ Resource group '$resourceGroup' created successfully in region '$region'."
}
catch {
Write-Host -ForegroundColor Red "❌ Failed to create resource group '$resourceGroup' in region '$region'."
Write-Host -ForegroundColor Red "Error: $_"
exit 1
}
}
Key Features:
- Creates resource groups in multiple regions
- Comprehensive error handling with try-catch blocks
- Visual feedback using color-coded output and emojis
- Exits on first error to prevent partial deployments
Step 2: Building the Virtual Network and Subnets
The second script (02-Create-VNet-Subnets.ps1) creates a VNet with two subnets: App (10.0.1.0/24) and Data (10.0.2.0/24). It includes validation to ensure subnet address ranges fall within the VNet address space.
# Configuration parameters
$resourceGroup = "rg-network-eastus"
$location = "eastus"
$vnetName = "vnet-prod-eastus"
$addressSpace = "10.0.0.0/16"
# Define subnets
$subnets = @(
@{
Name = "snet-app"
AddressPrefix = "10.0.1.0/24"
},
@{
Name = "snet-data"
AddressPrefix = "10.0.2.0/24"
}
)
# Validate subnet address prefixes fall within VNet address space
Write-Host -ForegroundColor Blue "🔍 Validating subnet address spaces..."
foreach ($subnet in $subnets) {
if (-not (Test-SubnetInVNet -VNetCIDR $addressSpace -SubnetCIDR $subnet.AddressPrefix)) {
Write-Host -ForegroundColor Red "❌ Subnet '$($subnet.Name)' address prefix '$($subnet.AddressPrefix)' is not within VNet address space '$addressSpace'"
exit 1
}
Write-Host -ForegroundColor Green "✅ Subnet '$($subnet.Name)' validated successfully"
}
# Create the Virtual Network
Write-Host -ForegroundColor Blue "🏗️ Creating Virtual Network '$vnetName' in resource group '$resourceGroup'..."
az network vnet create `
--resource-group $resourceGroup `
--name $vnetName `
--location $location `
--address-prefix $addressSpace | Out-Null
Write-Host -ForegroundColor Green "✅ Virtual Network '$vnetName' created successfully."
# Create each subnet
foreach ($subnet in $subnets) {
Write-Host -ForegroundColor Blue "🔧 Creating subnet '$($subnet.Name)' with address prefix '$($subnet.AddressPrefix)'..."
az network vnet subnet create `
--resource-group $resourceGroup `
--vnet-name $vnetName `
--name $subnet.Name `
--address-prefix $subnet.AddressPrefix | Out-Null
Write-Host -ForegroundColor Green "✅ Subnet '$($subnet.Name)' created successfully."
}
Key Features:
- Validates subnet CIDR blocks fall within VNet address space
- Creates VNet with customizable address space (10.0.0.0/16)
- Provisions two subnets for multi-tier architecture
- Includes detailed summary output after deployment
Step 3: Creating NSGs and Configuring Rules
The third script (03-Create-NSGs.ps1) is the most complex, creating NSGs and associating them with subnets, then configuring detailed security rules:
App Subnet NSG Rules:
- ✅ Allow HTTP (80) from Internet (Priority 100)
- ✅ Allow HTTPS (443) from Internet (Priority 110)
- ✅ Allow PostgreSQL (5432) to Data Subnet (Priority 100 Outbound)
- ❌ Deny all other traffic (Priority 4096)
Data Subnet NSG Rules:
- ✅ Allow PostgreSQL (5432) from App Subnet only (Priority 100)
- ❌ Deny all inbound and outbound traffic (Priority 4096)
# Define subnet configurations with NSG rules
$subnetsConfig = @{
"snet-app" = @{
InboundRules = @(
@{
Name = "AllowHTTPFromInternet"
Priority = 100
Direction = "Inbound"
Access = "Allow"
Protocol = "Tcp"
SourceAddressPrefixes = @("Internet")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("80")
},
@{
Name = "AllowHTTPSFromInternet"
Priority = 110
Direction = "Inbound"
Access = "Allow"
Protocol = "Tcp"
SourceAddressPrefixes = @("Internet")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("443")
},
@{
Name = "DenyAllInbound"
Priority = 4096
Direction = "Inbound"
Access = "Deny"
Protocol = "*"
SourceAddressPrefixes = @("*")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("*")
}
)
OutboundRules = @(
@{
Name = "AllowToDataSubnet"
Priority = 100
Direction = "Outbound"
Access = "Allow"
Protocol = "Tcp"
SourceAddressPrefixes = @("*")
DestinationAddressPrefixes = @("10.0.2.0/24")
DestinationPortRanges = @("5432")
},
@{
Name = "DenyAllOutbound"
Priority = 4096
Direction = "Outbound"
Access = "Deny"
Protocol = "*"
SourceAddressPrefixes = @("*")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("*")
}
)
}
"snet-data" = @{
InboundRules = @(
@{
Name = "AllowFromAppSubnet"
Priority = 100
Direction = "Inbound"
Access = "Allow"
Protocol = "Tcp"
SourceAddressPrefixes = @("10.0.1.0/24")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("5432")
},
@{
Name = "DenyAllInbound"
Priority = 4096
Direction = "Inbound"
Access = "Deny"
Protocol = "*"
SourceAddressPrefixes = @("*")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("*")
}
)
OutboundRules = @(
@{
Name = "DenyAllOutbound"
Priority = 4096
Direction = "Outbound"
Access = "Deny"
Protocol = "*"
SourceAddressPrefixes = @("*")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("*")
}
)
}
}
# Create NSGs and associate them with subnets
foreach ($subnetName in $subnetsConfig.Keys) {
$nsgName = "nsg-$vnetName-$subnetName"
Write-Host -ForegroundColor Blue "🔐 Creating NSG: $nsgName in resource group: $nsgResourceGroupName"
az network nsg create --location $vnetLocation --name $nsgName --resource-group $nsgResourceGroupName | Out-Null
Write-Host -ForegroundColor Green "✅ NSG '$nsgName' created successfully"
Write-Host -ForegroundColor Blue "🔗 Associating NSG: $nsgName with subnet: $subnetName"
az network vnet subnet update --name $subnetName --network-security-group $nsgName --resource-group $vnetResourceGroupName --vnet-name $vnetName | Out-Null
Write-Host -ForegroundColor Green "✅ NSG associated with subnet successfully"
# Create inbound rules
Write-Host -ForegroundColor Cyan "⬇️ Creating Inbound Rules for $nsgName"
foreach ($rule in $subnetsConfig[$subnetName].InboundRules) {
Create-NSGRule -resourceGroup $nsgResourceGroupName -nsgName $nsgName `
-ruleName $rule.Name `
-priority $rule.Priority `
-direction $rule.Direction `
-access $rule.Access `
-protocol $rule.Protocol `
-sourceAddressPrefixes $rule.SourceAddressPrefixes `
-destinationAddressPrefixes $rule.DestinationAddressPrefixes `
-destinationPortRanges $rule.DestinationPortRanges
}
# Create outbound rules
Write-Host -ForegroundColor Cyan "⬆️ Creating Outbound Rules for $nsgName"
foreach ($rule in $subnetsConfig[$subnetName].OutboundRules) {
Create-NSGRule -resourceGroup $nsgResourceGroupName -nsgName $nsgName `
-ruleName $rule.Name `
-priority $rule.Priority `
-direction $rule.Direction `
-access $rule.Access `
-protocol $rule.Protocol `
-sourceAddressPrefixes $rule.SourceAddressPrefixes `
-destinationAddressPrefixes $rule.DestinationAddressPrefixes `
-destinationPortRanges $rule.DestinationPortRanges
}
}
Key Features:
- Declarative configuration using PowerShell hashtables
- Reusable
Create-NSGRulefunction for consistency - Supports multiple source/destination prefixes and port ranges
- Visual feedback showing rule creation progress with emojis
Step 4: Master Deployment Script
The Deploy-All.ps1 script orchestrates the entire deployment by running all scripts in sequence. It includes confirmation prompts and comprehensive error handling.
Write-Host -ForegroundColor Cyan "🚀 Azure VNet & NSG Automation - Complete Deployment"
Write-Host -ForegroundColor Yellow "📋 This script will deploy:"
Write-Host " 1️⃣ Resource Groups in East US and West US"
Write-Host " 2️⃣ Virtual Network with App and Data subnets"
Write-Host " 3️⃣ Network Security Groups with security rules"
Write-Host -ForegroundColor Cyan "❓ Do you want to proceed? (yes/no): " -NoNewline
$confirm = Read-Host
if ($confirm -notmatch "^(yes|y)$") {
Write-Host -ForegroundColor Green "✅ Deployment canceled by user."
exit 0
}
try {
# Step 1: Create Resource Groups
Write-Host -ForegroundColor Magenta "📦 STEP 1: Creating Resource Groups"
& ".\01-Create-ResourceGroups.ps1"
# Step 2: Create VNet and Subnets
Write-Host -ForegroundColor Magenta "🌐 STEP 2: Creating VNet and Subnets"
& ".\02-Create-VNet-Subnets.ps1"
# Step 3: Create NSGs
Write-Host -ForegroundColor Magenta "🛡️ STEP 3: Creating Network Security Groups"
& ".\03-Create-NSGs.ps1"
Write-Host -ForegroundColor Green "🎉 DEPLOYMENT COMPLETED SUCCESSFULLY!"
Write-Host -ForegroundColor Cyan "✅ Your Azure network infrastructure is now deployed."
}
catch {
Write-Host -ForegroundColor Red "❌ DEPLOYMENT FAILED"
Write-Host -ForegroundColor Red "Error: $_"
Write-Host -ForegroundColor Yellow "⚠️ Please review the error message above and retry the deployment."
exit 1
}
Step 5: Cleanup Script
The cleanup script (04-Delete-ResourceGroups.ps1) safely deletes resource groups with multiple confirmation prompts to prevent accidental deletion.
Write-Host -ForegroundColor Cyan "🗑️ Azure Resource Group Deletion Script"
Write-Host -ForegroundColor Yellow "⚠️ WARNING: This will delete the following resource groups and ALL contained resources:"
foreach ($resourceGroup in $resourceGroups.Keys) {
Write-Host -ForegroundColor Yellow " ❌ $resourceGroup"
}
# Global confirmation
Write-Host -ForegroundColor Red "⛔ This operation is IRREVERSIBLE!"
Write-Host -ForegroundColor Cyan "❓ Do you want to proceed with deletion of ALL resource groups? (yes/no): " -NoNewline
$globalConfirm = Read-Host
if ($globalConfirm -notmatch "^(yes|y)$") {
Write-Host -ForegroundColor Green "✅ Operation canceled by user. No resource groups were deleted."
exit 0
}
# Delete each resource group
foreach ($resourceGroup in $resourceGroups.Keys) {
Write-Host -ForegroundColor Yellow "🔍 Checking if resource group '$resourceGroup' exists..."
$rgExists = az group exists --name $resourceGroup --output tsv
if ($rgExists -eq "true") {
Write-Host -ForegroundColor Blue "📦 Resource group '$resourceGroup' found."
Write-Host -ForegroundColor Cyan "❓ Confirm deletion of '$resourceGroup'? (yes/no): " -NoNewline
$confirmDelete = Read-Host
if ($confirmDelete -match "^(yes|y)$") {
Write-Host -ForegroundColor Blue "🗑️ Deleting resource group '$resourceGroup'..."
az group delete --name $resourceGroup --yes --output json --no-wait
Write-Host -ForegroundColor Green "✅ Deletion of resource group '$resourceGroup' initiated successfully."
Write-Host -ForegroundColor Yellow " ⏳ Note: Deletion is running in the background and may take several minutes to complete."
}
}
}
Key Features:
- Double confirmation (global + per-resource group)
- Checks if resource groups exist before attempting deletion
- Async deletion with
--no-waitfor faster execution - Clear warning messages to prevent accidental data loss
Quick Start Guide
To deploy this infrastructure in your Azure environment:
Option 1: Deploy Everything at Once
# Clone the repository
git clone https://github.com/kelomai/azure-vnet-nsg-automation.git
cd azure-vnet-nsg-automation
# Authenticate to Azure
az login
az account set --subscription "Your-Subscription-Name"
# Run the master deployment script
./Deploy-All.ps1
Option 2: Deploy Step-by-Step
# Step 1: Create Resource Groups
./01-Create-ResourceGroups.ps1
# Step 2: Create VNet and Subnets
./02-Create-VNet-Subnets.ps1
# Step 3: Create NSGs and Rules
./03-Create-NSGs.ps1
Verify Deployment
# List all VNets
az network vnet list --output table
# List all NSGs
az network nsg list --output table
# View NSG rules for App subnet
az network nsg rule list \
--resource-group rg-network-eastus \
--nsg-name nsg-vnet-prod-eastus-snet-app \
--output table
Best Practices for Network Automation
When automating Azure network deployments, follow these best practices:
- Use Infrastructure as Code: Store your PowerShell scripts in version control (Git) for tracking changes and collaboration.
- Parameterize Everything: Make your scripts flexible by using variables for resource names, regions, and address spaces.
- Implement Error Handling: Always include try-catch blocks and meaningful error messages for troubleshooting.
- Test in Non-Production: Validate all automation scripts in development/test environments before running in production.
- Document Your Scripts: Add comments explaining complex logic and NSG rule purposes.
- Use Naming Conventions: Follow consistent naming patterns (e.g.,
nsg-vnet-subnet) for easy identification.
Consider using Azure Policy to enforce NSG configuration standards across your organization.
Customization Guide
All scripts are designed to be easily customizable:
Change Resource Names
Edit variables at the top of each script:
$resourceGroup = "rg-network-eastus" # Your resource group name
$vnetName = "vnet-prod-eastus" # Your VNet name
$location = "eastus" # Your Azure region
Modify Network Address Spaces
Update CIDR blocks in 02-Create-VNet-Subnets.ps1:
$addressSpace = "10.0.0.0/16" # VNet address space
$subnets = @(
@{
Name = "snet-app"
AddressPrefix = "10.0.1.0/24" # App subnet
},
@{
Name = "snet-data"
AddressPrefix = "10.0.2.0/24" # Data subnet
}
)
Add Custom NSG Rules
Extend the $subnetsConfig hashtable in 03-Create-NSGs.ps1:
@{
Name = "AllowSSH"
Priority = 120
Direction = "Inbound"
Access = "Allow"
Protocol = "Tcp"
SourceAddressPrefixes = @("YourIP/32")
DestinationAddressPrefixes = @("*")
DestinationPortRanges = @("22")
}
Troubleshooting Common Issues
Permission Errors
Ensure your Azure account has Contributor or Network Contributor role:
az role assignment list --assignee your-email@domain.com --output table
Address Space Conflicts
Verify subnet CIDR blocks don’t overlap and fall within the VNet address space. The validation function in the script will catch most issues before deployment.
NSG Rule Conflicts
Check rule priorities - lower numbers are evaluated first. Ensure your allow rules have lower priority numbers than deny rules:
az network nsg rule list \
--resource-group rg-network-eastus \
--nsg-name nsg-vnet-prod-eastus-snet-app \
--query "[].{Name:name, Priority:priority, Access:access}" \
--output table
Resource Already Exists
Use unique names or delete existing resources before redeploying:
# Check if resource group exists
az group exists --name rg-network-eastus
# Delete if needed (use with caution!)
./04-Delete-ResourceGroups.ps1
Conclusion
By combining these PowerShell scripts, you can easily set up a highly secure Azure network. The automation not only provisions and configures the resources but also tightly controls network traffic using NSGs. This approach helps you achieve a robust security posture while simplifying management and reducing manual overhead.
This Infrastructure as Code approach ensures consistency across environments, reduces human error, and makes your Azure deployments repeatable and auditable. As your infrastructure grows, you can extend these scripts to include additional subnets, more complex NSG rules, or integration with Azure Firewall and Application Gateway.
Feel free to modify and extend these scripts as your environment grows, and always test changes in a non-production setting.
“Automation is not about replacing humans; it’s about freeing them to focus on innovation and strategy.”
For the complete sample files and full scripts, please visit our GitHub repository: Azure VNet & NSG Automation
Related Posts:
Happy automating!