Azure Firewall
What is Azure Firewall?
Azure Firewall is a cloud-native, fully stateful Network Virtual Appliance (NVA) that provides centralized network security across your Azure virtual networks. It's the recommended Layer 3-7 firewall solution for hub-spoke topologies.
Key Features:
- Built-in High Availability: No load balancer configuration needed
- Unrestricted Cloud Scalability: Scale based on traffic demands
- FQDN Filtering: Filter outbound HTTP/S traffic based on fully qualified domain names
- Threat Intelligence: Block traffic to/from known malicious IPs and domains (Microsoft's feed)
- DNAT (Destination NAT): Translate inbound internet traffic to private IPs
- SNAT (Source NAT): Translate outbound traffic to Azure Firewall public IP
- Multiple Public IPs: Support up to 250 public IP addresses
Azure Firewall vs NSG
| Feature | Azure Firewall | Network Security Group (NSG) |
|---|---|---|
| OSI Layer | L3-L7 (Network to Application) | L3-L4 (Network & Transport) |
| Filtering | FQDN, IP, port, protocol | IP, port, protocol only |
| Centralized | Hub-based, protects entire VNet | Distributed per subnet/NIC |
| Threat Intelligence | ✅ Built-in | ❌ Not available |
| Logging | Diagnostic logs, Azure Monitor | NSG Flow Logs |
| Cost | $$$ Hourly + data processed | $ Free (just storage for logs) |
| Use Case | Hub firewall, application filtering | Micro-segmentation, defense-in-depth |
Best Practice: Use both - Azure Firewall for centralized control, NSG for micro-segmentation
How Azure Firewall Works
Hub-Spoke Architecture
Internet
│
▼
[Azure Firewall] ← Public IP
│
┌──────┴──────┐
│ Hub VNet │
│ 10.0.0.0/16 │
└──────┬───────┘
│ Peering
┌──────┴──────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Spoke 1 │ │ Spoke 2 │
│10.1.0.0 │ │10.2.0.0 │
└─────────┘ └─────────┘
Traffic Flow:
- Outbound: Spoke → Hub (via route table) → Azure Firewall → Internet
- Inbound: Internet → Azure Firewall (DNAT) → Spoke
- Spoke-to-Spoke: Spoke 1 → Hub → Azure Firewall → Spoke 2
Rule Processing Order
Azure Firewall processes rules in this sequence:
- NAT Rules (DNAT for inbound)
- Network Rules (Layer 3/4 filtering)
- Application Rules (Layer 7 FQDN filtering)
Important: First match wins! Order rules from most to least specific.
How to Deploy Azure Firewall
Prerequisites
# Create resource group
az group create \
--name rg-network-hub \
--location eastus
# Create hub VNet with dedicated firewall subnet
az network vnet create \
--resource-group rg-network-hub \
--name vnet-hub-prod \
--address-prefix 10.0.0.0/16 \
--subnet-name AzureFirewallSubnet \ # MUST be exactly this name
--subnet-prefix 10.0.1.0/24 # Minimum /26 subnet
Deploy Azure Firewall
Using Azure CLI
# Create public IP for firewall
az network public-ip create \
--resource-group rg-network-hub \
--name pip-firewall-prod \
--sku Standard \
--allocation-method Static
# Create Azure Firewall
az network firewall create \
--resource-group rg-network-hub \
--name afw-hub-prod \
--location eastus \
--enable-dns-proxy true
# Configure firewall with public IP and VNet
az network firewall ip-config create \
--resource-group rg-network-hub \
--firewall-name afw-hub-prod \
--name FW-Config \
--public-ip-address pip-firewall-prod \
--vnet-name vnet-hub-prod
# Get firewall private IP (needed for route tables)
FW_PRIVATE_IP=$(az network firewall show \
--resource-group rg-network-hub \
--name afw-hub-prod \
--query \"ipConfigurations[0].privateIPAddress\" -o tsv)
echo \"Firewall Private IP: $FW_PRIVATE_IP\"
Create Firewall Policy
# Create firewall policy
az network firewall policy create \
--resource-group rg-network-hub \
--name afwp-hub-prod \
--sku Standard \
--threat-intel-mode Alert # Alert, Deny, or Off
# Associate policy with firewall
az network firewall update \
--resource-group rg-network-hub \
--name afw-hub-prod \
--firewall-policy afwp-hub-prod
Configure Route Tables
# Create route table for spoke subnets
az network route-table create \
--resource-group rg-network-hub \
--name rt-spoke-to-firewall
# Add default route through firewall
az network route-table route create \
--resource-group rg-network-hub \
--route-table-name rt-spoke-to-firewall \
--name default-via-firewall \
--address-prefix 0.0.0.0/0 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address $FW_PRIVATE_IP
# Associate route table with spoke subnet
az network vnet subnet update \
--resource-group rg-network-spoke1 \
--vnet-name vnet-spoke1-prod \
--name snet-app \
--route-table rt-spoke-to-firewall
Terraform Example
# Hub VNet
resource \"azurerm_virtual_network\" \"hub\" {\n name = \"vnet-hub-prod\"
address_space = [\"10.0.0.0/16\"]
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
}
# Firewall Subnet (name MUST be AzureFirewallSubnet)
resource \"azurerm_subnet\" \"firewall\" {\n name = \"AzureFirewallSubnet\"
resource_group_name = azurerm_resource_group.hub.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [\"10.0.1.0/24\"] # Minimum /26
}
# Public IP for Firewall
resource \"azurerm_public_ip\" \"firewall\" {\n name = \"pip-firewall-prod\"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
allocation_method = \"Static\"
sku = \"Standard\"
}
# Firewall Policy
resource \"azurerm_firewall_policy\" \"hub\" {\n name = \"afwp-hub-prod\"
resource_group_name = azurerm_resource_group.hub.name
location = azurerm_resource_group.hub.location
sku = \"Standard\" # Standard or Premium
threat_intelligence_mode = \"Alert\" # Alert, Deny, or Off
dns {
proxy_enabled = true # Enable DNS proxy
}
insights {
enabled = true
retention_in_days = 90
default_log_analytics_workspace_id = azurerm_log_analytics_workspace.security.id
}
}
# Azure Firewall
resource \"azurerm_firewall\" \"hub\" {\n name = \"afw-hub-prod\"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
sku_name = \"AZFW_VNet\"
sku_tier = \"Standard\" # Standard or Premium
firewall_policy_id = azurerm_firewall_policy.hub.id
ip_configuration {
name = \"fw-ipconfig\"
subnet_id = azurerm_subnet.firewall.id
public_ip_address_id = azurerm_public_ip.firewall.id
}
}
# Application Rule Collection (L7 filtering)
resource \"azurerm_firewall_policy_rule_collection_group\" \"app_rules\" {\n name = \"DefaultApplicationRuleCollectionGroup\"
firewall_policy_id = azurerm_firewall_policy.hub.id
priority = 100
application_rule_collection {
name = \"AllowCriticalSites\"
priority = 100
action = \"Allow\"
rule {
name = \"AllowMicrosoft\"
source_addresses = [\"10.1.0.0/16\", \"10.2.0.0/16\"] # Spoke VNets
protocols {
type = \"Https\"
port = 443
}
destination_fqdns = [
\"*.microsoft.com\",
\"*.windows.net\",
\"*.azure.com\"
]
}
rule {
name = \"AllowPackageManagers\"
source_addresses = [\"10.1.0.0/16\"]
protocols {
type = \"Https\"
port = 443
}
destination_fqdns = [
\"*.ubuntu.com\",
\"*.npmjs.org\",
\"*.pypi.org\",
\"github.com\"
]
}
}
}
# Network Rule Collection (L3/L4 filtering)
resource \"azurerm_firewall_policy_rule_collection_group\" \"network_rules\" {\n name = \"DefaultNetworkRuleCollectionGroup\"
firewall_policy_id = azurerm_firewall_policy.hub.id
priority = 200
network_rule_collection {
name = \"AllowOutbound\"
priority = 200
action = \"Allow\"
rule {
name = \"AllowDNS\"
protocols = [\"UDP\"]
source_addresses = [\"*\"]
destination_addresses = [\"8.8.8.8\", \"8.8.4.4\"] # Google DNS
destination_ports = [\"53\"]
}
rule {
name = \"AllowNTP\"
protocols = [\"UDP\"]
source_addresses = [\"*\"]
destination_fqdns = [\"time.windows.com\"]
destination_ports = [\"123\"]
}
}
}
# NAT Rule Collection (Inbound DNAT)
resource \"azurerm_firewall_policy_rule_collection_group\" \"nat_rules\" {\n name = \"DefaultDnatRuleCollectionGroup\"
firewall_policy_id = azurerm_firewall_policy.hub.id
priority = 300
nat_rule_collection {
name = \"InboundDNAT\"
priority = 300
action = \"Dnat\"
rule {
name = \"AllowHTTPSInbound\"
protocols = [\"TCP\"]
source_addresses = [\"*\"] # Public internet
destination_address = azurerm_public_ip.firewall.ip_address
destination_ports = [\"443\"]
translated_address = \"10.1.1.4\" # Internal web server
translated_port = \"443\"
}
rule {
name = \"AllowSSHInbound\"
protocols = [\"TCP\"]
source_addresses = [\"203.0.113.0/24\"] # Your office IP range
destination_address = azurerm_public_ip.firewall.ip_address
destination_ports = [\"22000\"] # Non-standard port
translated_address = \"10.1.2.10\" # Jump box
translated_port = \"22\"
}
}
}
# Route Table to send spoke traffic through firewall
resource \"azurerm_route_table\" \"spoke_to_firewall\" {\n name = \"rt-spoke-to-firewall\"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
route {
name = \"default-via-firewall\"
address_prefix = \"0.0.0.0/0\"
next_hop_type = \"VirtualAppliance\"
next_hop_in_ip_address = azurerm_firewall.hub.ip_configuration[0].private_ip_address
}
}
# Associate route table with spoke subnet
resource \"azurerm_subnet_route_table_association\" \"spoke1_app\" {\n subnet_id = azurerm_subnet.spoke1_app.id
route_table_id = azurerm_route_table.spoke_to_firewall.id
}
Using IP Groups for Management
IP Groups simplify rule management:
# Define IP groups
resource \"azurerm_ip_group\" \"spoke_vnets\" {\n name = \"ipg-spoke-vnets\"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
cidrs = [
\"10.1.0.0/16\",
\"10.2.0.0/16\",
\"10.3.0.0/16\"
]
}
resource \"azurerm_ip_group\" \"dmz_servers\" {\n name = \"ipg-dmz-servers\"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
cidrs = [
\"10.1.1.4/32\",
\"10.1.1.5/32\"
]
}
# Use IP groups in rules
application_rule_collection {
name = \"AllowDMZ\"
priority = 110
action = \"Allow\"
rule {
name = \"DMZOutbound\"
source_ip_groups = [azurerm_ip_group.dmz_servers.id]
protocols {
type = \"Https\"
port = 443
}
destination_fqdns = [\"*.api.example.com\"]
}
}
CI/CD Integration
Deploy Firewall with GitHub Actions
name: Deploy Azure Firewall
on:
push:
paths:
- 'terraform/firewall/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Terraform Init
run: |\n cd terraform/firewall
terraform init
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Terraform Apply
run: terraform apply -auto-approve tfplan
Validate Firewall Rules
- name: Validate Firewall Policy
run: |\n # Check for overly permissive rules
az network firewall policy rule-collection-group list \\\n --resource-group rg-network-hub \\\n --policy-name afwp-hub-prod \\\n --query \"[].{Name:name, Priority:priority}\" -o table
# Ensure threat intelligence is enabled
THREAT_INTEL=$(az network firewall policy show \\\n --resource-group rg-network-hub \\\n --name afwp-hub-prod \\\n --query threatIntelMode -o tsv)
if [ \"$THREAT_INTEL\" != \"Alert\" ] && [ \"$THREAT_INTEL\" != \"Deny\" ]; then
echo \"ERROR: Threat Intelligence not enabled!\"
exit 1
fi
Best Practices
1. Use Firewall Policy (Not Classic Rules)
✅ Firewall Policy (modern approach):
- Centralized management
- Support for rule hierarchies (parent/child policies)
- Better for multi-firewall deployments
❌ Classic Rules (deprecated):
- Managed per firewall
- Difficult to maintain at scale
2. Enable Threat Intelligence
threat_intelligence_mode = \"Alert\" # or \"Deny\" for blocking
Alert = Log suspicious traffic Deny = Block malicious IPs/domains (Microsoft threat feed)
3. Use DNS Proxy
dns {
proxy_enabled = true
}
Benefits:
- Firewall resolves FQDN rules using its own DNS
- Prevents DNS cache poisoning attacks
- Required for FQDN-based network rules
4. Organize Rules with Priority
Priority 100-199: Critical business apps
Priority 200-299: General outbound access
Priority 300-399: Management/monitoring
Priority 400-499: Development/testing
Lower number = higher priority = processed first
5. Enable Diagnostic Logging
resource \"azurerm_monitor_diagnostic_setting\" \"firewall\" {\n name = \"firewall-diagnostics\"
target_resource_id = azurerm_firewall.hub.id
log_analytics_workspace_id = azurerm_log_analytics_workspace.security.id
enabled_log {
category = \"AzureFirewallApplicationRule\"
}
enabled_log {
category = \"AzureFirewallNetworkRule\"
}
enabled_log {
category = \"AzureFirewallDnsProxy\"
}
metric {
category = \"AllMetrics\"
enabled = true
}
}
Common Use Cases
1. Hub-Spoke with Forced Tunneling
# Force all spoke traffic through firewall
resource \"azurerm_route\" \"default_route\" {\n name = \"default-via-firewall\"
resource_group_name = azurerm_resource_group.hub.name
route_table_name = azurerm_route_table.spoke.name
address_prefix = \"0.0.0.0/0\"
next_hop_type = \"VirtualAppliance\"
next_hop_in_ip_address = azurerm_firewall.hub.ip_configuration[0].private_ip_address
}
2. Allow Specific SaaS Applications
application_rule_collection {
name = \"AllowSaaS\"
priority = 120
action = \"Allow\"
rule {
name = \"AllowOffice365\"
source_addresses = [\"10.1.0.0/16\"]
protocols {
type = \"Https\"
port = 443
}
destination_fqdn_tags = [\"Office365\"] # Simplified FQDN tag
}
}
Built-in FQDN Tags: WindowsUpdate, AzureBackup, AzureKubernetesService, MicrosoftActiveProtectionService
3. Inbound Web Traffic (DNAT)
nat_rule_collection {
name = \"PublicWebAccess\"
priority = 300
action = \"Dnat\"
rule {
name = \"HTTPSInbound\"
protocols = [\"TCP\"]
source_addresses = [\"*\"]
destination_address = azurerm_public_ip.firewall.ip_address
destination_ports = [\"443\"]
translated_address = \"10.1.1.10\" # Application Gateway internal IP
translated_port = \"443\"
}
}
Things to Avoid
❌ Don't use 0.0.0.0/0 as source in allow rules (too permissive)
❌ Don't skip threat intelligence (free protection against known threats)
❌ Don't disable diagnostic logging (blind to attacks)
❌ Don't use Classic Rules (use Firewall Policy instead)
❌ Don't forget to update route tables after deploying firewall
❌ Don't expose SSH/RDP on standard ports (use DNAT with non-standard ports)
❌ Don't allow all outbound HTTPS without FQDN filtering
❌ Don't ignore Azure Firewall Premium for TLS inspection (if handling sensitive data)
✅ Do use specific source/destination addresses ✅ Do enable threat intelligence (Alert or Deny) ✅ Do use IP Groups for easier management ✅ Do enable DNS proxy for FQDN filtering ✅ Do send logs to Log Analytics/Sentinel ✅ Do use FQDN tags (WindowsUpdate, Office365, etc.) ✅ Do implement defense-in-depth (Firewall + NSG) ✅ Do use Azure Firewall Premium for advanced threats (TLS inspection, IDPS)
Monitoring and Troubleshooting
Check Firewall Logs (KQL)
// Application rule hits
AzureDiagnostics
| where Category == \"AzureFirewallApplicationRule\"
| where TimeGenerated > ago(1h)
| parse msg_s with Protocol \" request from \" SourceIP \":\" SourcePort \" to \" FQDN \":\" DestinationPort \". Action: \" Action \".\"
| project TimeGenerated, Protocol, SourceIP, FQDN, DestinationPort, Action
| order by TimeGenerated desc
// Network rule hits
AzureDiagnostics
| where Category == \"AzureFirewallNetworkRule\"
| where TimeGenerated > ago(1h)
| parse msg_s with Protocol \" request from \" SourceIP \":\" SourcePort \" to \" DestinationIP \":\" DestinationPort \". Action: \" Action
| project TimeGenerated, Protocol, SourceIP, DestinationIP, DestinationPort, Action
| order by TimeGenerated desc
// Top blocked destinations
AzureDiagnostics
| where Category == \"AzureFirewallApplicationRule\"
| where msg_s contains \"Deny\"
| parse msg_s with * \" to \" FQDN \":\" *
| summarize DeniedRequests = count() by FQDN
| top 20 by DeniedRequests desc
Test Connectivity Through Firewall
# From a VM in spoke VNet
# Check if traffic is routing through firewall
traceroute google.com
# Expected: First hop should be firewall private IP (e.g., 10.0.1.4)
# Test specific connectivity
curl -v https://www.microsoft.com
# If blocked, check application rules
Common Errors
Error: "VM cannot reach internet"
- Cause: Missing route table or incorrect next-hop IP
- Fix: Verify route table has
0.0.0.0/0→ firewall private IP
Error: "DNAT not working"
- Cause: NAT rule priority too low, or wrong destination IP
- Fix: Ensure NAT rules are priority 100-199, use firewall's public IP as destination
Error: "FQDN filtering not working"
- Cause: DNS proxy disabled
- Fix: Enable DNS proxy in firewall policy
Cost Optimization
Pricing (Standard tier, East US):
- Deployment: $1.25/hour (~$912/month)
- Data processed: $0.016/GB
Example (100GB/day outbound):
- Deployment: $912/month
- Data: 100GB × 30 days × $0.016 = $48/month
- Total: ~$960/month
Tips to reduce cost:
- Use Basic tier for dev/test ($0.50/hour)
- Filter unnecessary traffic at NSG before reaching firewall
- Use Azure Firewall Manager to share firewall across multiple subscriptions
- Stop firewall in non-production environments outside business hours