Skip to main content

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

FeatureAzure FirewallNetwork Security Group (NSG)
OSI LayerL3-L7 (Network to Application)L3-L4 (Network & Transport)
FilteringFQDN, IP, port, protocolIP, port, protocol only
CentralizedHub-based, protects entire VNetDistributed per subnet/NIC
Threat Intelligence✅ Built-in❌ Not available
LoggingDiagnostic logs, Azure MonitorNSG Flow Logs
Cost$$$ Hourly + data processed$ Free (just storage for logs)
Use CaseHub firewall, application filteringMicro-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:

  1. Outbound: Spoke → Hub (via route table) → Azure Firewall → Internet
  2. Inbound: Internet → Azure Firewall (DNAT) → Spoke
  3. Spoke-to-Spoke: Spoke 1 → Hub → Azure Firewall → Spoke 2

Rule Processing Order

Azure Firewall processes rules in this sequence:

  1. NAT Rules (DNAT for inbound)
  2. Network Rules (Layer 3/4 filtering)
  3. 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