← Back to all guides

Azure Zero Trust Network Setup Guide

Infrastructure · 2026-03-22

Overview

Zero Trust is a security model built on the principle "never trust, always verify." Every request — regardless of origin — is authenticated, authorized, and encrypted before access is granted. This guide walks through setting up a Zero Trust network architecture on Azure using four key services:

  1. Azure Traffic Manager — DNS-based global traffic routing
  2. Azure Application Gateway (with WAF) — Layer 7 load balancing and web application firewall
  3. Azure Firewall — Managed network-layer firewall for east-west and north-south traffic
  4. Azure Firewall Premium — Advanced threat protection with TLS inspection and IDPS

Official Reference: Microsoft Zero Trust deployment guide


Architecture Diagram

The diagram below shows how traffic flows through the Zero Trust network stack. (View full SVG diagram)

┌─────────────────────────────────────────────────────────────────────────────┐
│                          INTERNET / END USERS                               │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
                                   ▼
              ┌─────────────────────────────────────────┐
              │       AZURE TRAFFIC MANAGER              │
              │   (DNS-based global load balancing)      │
              │                                         │
              │  • Health probes per region              │
              │  • Priority / Weighted / Geographic      │
              │  • Failover routing                      │
              └────────────┬───────────────┬────────────┘
                           │               │
                    Region A               Region B
                           │               │
                           ▼               ▼
              ┌────────────────────────────────────────┐
              │     APPLICATION GATEWAY + WAF v2        │
              │      (Layer 7 — per region)             │
              │                                        │
              │  • SSL/TLS termination                 │
              │  • OWASP Core Rule Set 3.2             │
              │  • Path-based routing                  │
              │  • Custom WAF policies                 │
              │  • End-to-end TLS re-encryption        │
              └────────────────────┬───────────────────┘
                                   │
                                   ▼
              ┌────────────────────────────────────────┐
              │       AZURE FIREWALL (PREMIUM)          │
              │    (Layer 3-7 — hub VNet)               │
              │                                        │
              │  • TLS inspection (decrypt/inspect)    │
              │  • IDPS (signature-based detection)    │
              │  • URL filtering & web categories      │
              │  • Threat intelligence feed            │
              │  • Network & application rules         │
              └────────────────────┬───────────────────┘
                                   │
                          ┌────────┴────────┐
                          │                 │
                          ▼                 ▼
              ┌──────────────────┐ ┌──────────────────┐
              │   Spoke VNet 1   │ │   Spoke VNet 2   │
              │  ┌────────────┐  │ │  ┌────────────┐  │
              │  │ App Service │  │ │  │  AKS / VMs │  │
              │  │ Environment │  │ │  │  Workloads │  │
              │  └────────────┘  │ │  └────────────┘  │
              │  ┌────────────┐  │ │  ┌────────────┐  │
              │  │  Azure SQL  │  │ │  │  Storage   │  │
              │  │  (Private)  │  │ │  │ (Private)  │  │
              │  └────────────┘  │ │  └────────────┘  │
              └──────────────────┘ └──────────────────┘

Traffic Flow Summary:

  1. User DNS query resolves via Traffic Manager → routes to nearest healthy region
  2. Application Gateway terminates TLS, inspects HTTP with WAF rules, re-encrypts
  3. Azure Firewall Premium inspects traffic with IDPS and TLS inspection
  4. Traffic reaches workloads in spoke VNets via private endpoints

Zero Trust Principles Applied

Each service enforces specific Zero Trust principles:

PrincipleTraffic ManagerApp GatewayAzure Firewall
Verify ExplicitlyHealth probes verify endpoint availabilityWAF validates every HTTP request against OWASP rulesIDPS inspects every packet for known threat signatures
Least PrivilegeRoutes only to healthy, approved endpointsPath-based routing limits access to specific backendsDeny-all default with explicit allow rules only
Assume BreachAutomatic failover on endpoint compromiseBot protection and rate limiting block attack patternsTLS inspection catches encrypted threats; threat intel blocks known-bad IPs

Part 1: Azure Traffic Manager

Traffic Manager provides DNS-level routing across regions. It does not see or process actual traffic — it directs users to the closest or healthiest endpoint.

Official Docs: Azure Traffic Manager overview

1.1 Create a Traffic Manager Profile

Portal:

  1. Go to Azure PortalCreate a resource → search Traffic Manager profile
  2. Configure:
    • Name: zt-traffic-manager (becomes zt-traffic-manager.trafficmanager.net)
    • Routing method: Priority (for active-passive failover) or Performance (for latency-based)
    • Resource group: your Zero Trust resource group
    • Subscription: select your subscription

Azure CLI:

# Create resource group
az group create \
  --name rg-zerotrust-network \
  --location eastus

# Create Traffic Manager profile with priority routing
az network traffic-manager profile create \
  --name zt-traffic-manager \
  --resource-group rg-zerotrust-network \
  --routing-method Priority \
  --unique-dns-name zt-traffic-manager \
  --monitor-protocol HTTPS \
  --monitor-port 443 \
  --monitor-path "/health" \
  --ttl 30

Docs: Create a Traffic Manager profile — CLI

1.2 Configure Health Probes (Zero Trust: Verify Explicitly)

Health probes continuously verify that endpoints are alive and responding correctly. This is critical for Zero Trust — never assume an endpoint is healthy.

# Health probe settings are set during profile creation above.
# Key parameters:
#   --monitor-protocol HTTPS     (always use HTTPS, never HTTP)
#   --monitor-port 443
#   --monitor-path "/health"     (dedicated health endpoint)
#   --ttl 30                     (check every 30 seconds)

Best Practice Checklist:

  • Always use HTTPS for probes (never HTTP)
  • Set a dedicated /health endpoint that validates backend dependencies
  • Set TTL to 30 seconds or less for fast failover
  • Enable Fast Failover settings: probing interval = 10s, tolerated failures = 3

1.3 Add Endpoints

Add your Application Gateway public IPs as endpoints:

# Add primary region endpoint (Priority 1)
az network traffic-manager endpoint create \
  --name endpoint-eastus \
  --profile-name zt-traffic-manager \
  --resource-group rg-zerotrust-network \
  --type azureEndpoints \
  --target-resource-id /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Network/publicIPAddresses/appgw-pip-eastus \
  --priority 1

# Add secondary region endpoint (Priority 2 — failover)
az network traffic-manager endpoint create \
  --name endpoint-westus \
  --profile-name zt-traffic-manager \
  --resource-group rg-zerotrust-network \
  --type azureEndpoints \
  --target-resource-id /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Network/publicIPAddresses/appgw-pip-westus \
  --priority 2

1.4 Routing Method Comparison

MethodUse CaseZero Trust BenefitDocs Link
PriorityActive-passive DRAutomatic failover away from compromised regionsPriority routing
PerformanceLatency-based routingReduces attack surface by routing to nearest POPPerformance routing
GeographicData sovereigntyEnforces geo-based access control at DNS layerGeographic routing
WeightedBlue-green deploysCanary rollouts limit blast radius of bad deploysWeighted routing

1.5 Zero Trust Hardening for Traffic Manager

  • Disable HTTP endpoints — only allow HTTPS-monitored endpoints
  • Use nested profiles for multi-layer routing (geographic → performance)
  • Enable diagnostic logging → route to Log Analytics workspace
  • Lock the resource with Azure Resource Lock to prevent accidental deletion
  • RBAC: Grant Traffic Manager Contributor only to network admins
# Enable diagnostic logging
az monitor diagnostic-settings create \
  --name zt-tm-diagnostics \
  --resource /subscriptions/{sub-id}/resourceGroups/rg-zerotrust-network/providers/Microsoft.Network/trafficManagerProfiles/zt-traffic-manager \
  --workspace /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{workspace-name} \
  --logs '[{"category": "ProbeHealthStatusEvents", "enabled": true}]'

Docs: Traffic Manager diagnostics


Part 2: Azure Application Gateway with WAF v2

Application Gateway is a Layer 7 (HTTP/HTTPS) load balancer with an integrated Web Application Firewall (WAF). It sits in front of your workloads and inspects every request.

Official Docs: Application Gateway overview

Zero Trust Reference: Apply Zero Trust to Azure networking — Application Gateway

2.1 Prerequisites

Before creating the Application Gateway:

  • Dedicated subnet: Application Gateway requires its own subnet (minimum /24 recommended)
  • Public IP: Standard SKU, static allocation
  • WAF v2 SKU: Required for Zero Trust (Standard SKU does not include WAF)
# Create the VNet and Application Gateway subnet
az network vnet create \
  --name vnet-hub-eastus \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --address-prefixes 10.0.0.0/16

az network vnet subnet create \
  --name snet-appgw \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-hub-eastus \
  --address-prefixes 10.0.1.0/24

# Create static public IP
az network public-ip create \
  --name appgw-pip-eastus \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --sku Standard \
  --allocation-method Static

2.2 Create the Application Gateway (WAF v2)

az network application-gateway create \
  --name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --sku WAF_v2 \
  --capacity 2 \
  --vnet-name vnet-hub-eastus \
  --subnet snet-appgw \
  --public-ip-address appgw-pip-eastus \
  --http-settings-cookie-based-affinity Disabled \
  --frontend-port 443 \
  --http-settings-port 443 \
  --http-settings-protocol Https \
  --priority 100

Docs: Create Application Gateway — CLI

2.3 Configure TLS/SSL (Zero Trust: Encrypt Everything)

Never terminate TLS without re-encrypting to backend. Use end-to-end TLS:

# Upload TLS certificate for frontend
az network application-gateway ssl-cert create \
  --gateway-name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --name frontend-cert \
  --cert-file ./certs/frontend.pfx \
  --cert-password "{your-cert-password}"

# Configure backend HTTPS settings (re-encrypt to backend)
az network application-gateway http-settings update \
  --gateway-name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --name appGatewayBackendHttpSettings \
  --protocol Https \
  --port 443 \
  --host-name-from-backend-pool true

TLS Best Practices for Zero Trust:

  • Use TLS 1.2 minimum (TLS 1.3 preferred)
  • Store certificates in Azure Key Vault and reference them from App Gateway
  • Enable Key Vault integration for automatic certificate rotation
  • Use managed identity for App Gateway to access Key Vault
# Create managed identity for App Gateway
az identity create \
  --name id-appgw-zerotrust \
  --resource-group rg-zerotrust-network

# Grant Key Vault access to the managed identity
az keyvault set-policy \
  --name kv-zerotrust \
  --object-id {managed-identity-principal-id} \
  --secret-permissions get list

Docs: TLS termination with Key Vault certificates

2.4 Configure WAF Policy (Zero Trust: Verify Explicitly)

The WAF inspects every HTTP request. In Zero Trust, the WAF is your first line of defence at Layer 7.

# Create WAF policy in Prevention mode
az network application-gateway waf-policy create \
  --name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --location eastus

# Configure managed rule set (OWASP 3.2 + Bot Manager)
az network application-gateway waf-policy managed-rule rule-set add \
  --policy-name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --type OWASP \
  --version 3.2

az network application-gateway waf-policy managed-rule rule-set add \
  --policy-name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --type Microsoft_BotManagerRuleSet \
  --version 1.0

# Set WAF to Prevention mode (blocks attacks, not just detects)
az network application-gateway waf-policy policy-setting update \
  --policy-name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --state Enabled \
  --mode Prevention \
  --max-request-body-size-kb 128 \
  --file-upload-limit-mb 100 \
  --request-body-check true

# Associate WAF policy with Application Gateway
az network application-gateway update \
  --name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --waf-policy waf-policy-zerotrust

Docs: Configure WAF policies

2.5 WAF Custom Rules (Zero Trust: Assume Breach)

Add custom rules to block known attack patterns:

# Block requests from specific geolocations
az network application-gateway waf-policy custom-rule create \
  --policy-name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name GeoBlock \
  --priority 10 \
  --rule-type MatchRule \
  --action Block

# Rate limiting rule — block IPs making more than 100 requests per minute
az network application-gateway waf-policy custom-rule create \
  --policy-name waf-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name RateLimit \
  --priority 20 \
  --rule-type RateLimitRule \
  --rate-limit-threshold 100 \
  --rate-limit-duration FiveMins \
  --action Block

Docs: WAF custom rules

2.6 Path-Based Routing (Zero Trust: Least Privilege)

Route traffic to specific backend pools based on URL path. This limits each backend to only the traffic it should receive:

# Create backend pools for different services
az network application-gateway address-pool create \
  --gateway-name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --name pool-api \
  --servers 10.0.10.4 10.0.10.5

az network application-gateway address-pool create \
  --gateway-name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --name pool-web \
  --servers 10.0.11.4 10.0.11.5

# Create URL path map
az network application-gateway url-path-map create \
  --gateway-name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --name url-path-map \
  --paths "/api/*" \
  --address-pool pool-api \
  --default-address-pool pool-web \
  --http-settings appGatewayBackendHttpSettings \
  --default-http-settings appGatewayBackendHttpSettings

2.7 Enable Diagnostics and Logging

# Enable full diagnostic logging for Application Gateway
az monitor diagnostic-settings create \
  --name appgw-diagnostics \
  --resource /subscriptions/{sub-id}/resourceGroups/rg-zerotrust-network/providers/Microsoft.Network/applicationGateways/appgw-zerotrust-eastus \
  --workspace /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{workspace-name} \
  --logs '[
    {"category": "ApplicationGatewayAccessLog", "enabled": true},
    {"category": "ApplicationGatewayFirewallLog", "enabled": true},
    {"category": "ApplicationGatewayPerformanceLog", "enabled": true}
  ]' \
  --metrics '[{"category": "AllMetrics", "enabled": true}]'

Docs: Application Gateway diagnostics

2.8 Application Gateway Zero Trust Checklist

  • WAF v2 SKU with Prevention mode enabled
  • OWASP Core Rule Set 3.2 active
  • Bot Manager rule set enabled
  • End-to-end TLS — re-encrypt traffic to backends
  • TLS certificates stored in Key Vault with managed identity
  • TLS 1.2 minimum policy configured
  • Custom rate limiting rules in place
  • Path-based routing isolates backends (least privilege)
  • Diagnostic logs flowing to Log Analytics
  • Request body inspection enabled
  • Private backend pools only (no public IPs on backends)

Part 3: Azure Firewall (Standard and Premium)

Azure Firewall is a managed, cloud-native network security service. It filters traffic at Layers 3–7 and serves as the central enforcement point in a hub-and-spoke network. Firewall Premium adds TLS inspection and IDPS for full Zero Trust traffic inspection.

Official Docs: Azure Firewall overview

Zero Trust Reference: Apply Zero Trust to Azure Firewall

Standard vs Premium — Which Do You Need?

FeatureStandardPremiumZero Trust Impact
Network RulesYesYesDeny-all default posture
Application Rules (FQDN)YesYesAllow only specific domains
Threat IntelligenceAlert onlyAlert & denyBlock known malicious IPs/domains
TLS InspectionNoYesDecrypt and inspect encrypted traffic
IDPSNoYesSignature-based intrusion detection
URL FilteringFQDN onlyFull URL pathGranular URL-level access control
Web CategoriesFQDN-basedFull URL-basedBlock entire categories (gambling, malware)

Recommendation: For true Zero Trust, use Azure Firewall Premium. TLS inspection and IDPS are essential — without them, encrypted malicious traffic passes through uninspected.

Docs: Azure Firewall Premium features

3.1 Create the Firewall Subnet and Public IP

Azure Firewall requires a dedicated subnet named exactly AzureFirewallSubnet:

# Create the mandatory firewall subnet (minimum /26)
az network vnet subnet create \
  --name AzureFirewallSubnet \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-hub-eastus \
  --address-prefixes 10.0.2.0/26

# Create public IP for the firewall
az network public-ip create \
  --name fw-pip-eastus \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --sku Standard \
  --allocation-method Static

3.2 Create a Firewall Policy (Premium)

Firewall Policy is the recommended way to manage rules. It supports inheritance and can be shared across firewalls:

# Create Premium firewall policy
az network firewall policy create \
  --name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --sku Premium \
  --threat-intel-mode Deny \
  --intrusion-detection-mode Alert

# Enable IDPS in Alert & Deny mode
az network firewall policy intrusion-detection add \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --mode "Alert and Deny"

Docs: Firewall Policy overview

3.3 Configure TLS Inspection (Premium Only)

TLS inspection is the cornerstone of Zero Trust at the network layer. Without it, the firewall cannot inspect encrypted traffic.

Prerequisites:

  • An intermediate CA certificate stored in Azure Key Vault
  • Azure Firewall's managed identity must have access to Key Vault
# Create managed identity for the firewall
az identity create \
  --name id-fw-zerotrust \
  --resource-group rg-zerotrust-network

# Grant Key Vault access
az keyvault set-policy \
  --name kv-zerotrust \
  --object-id {firewall-managed-identity-principal-id} \
  --certificate-permissions get list \
  --secret-permissions get list

# Configure TLS inspection on the firewall policy
az network firewall policy update \
  --name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --key-vault-secret-id https://kv-zerotrust.vault.azure.net/secrets/intermediate-ca \
  --certificate-name intermediate-ca \
  --identity-type UserAssigned \
  --user-assigned-identity id-fw-zerotrust

TLS Inspection Flow:

Client                Azure Firewall Premium              Backend Server
  │                          │                                 │
  │──── TLS handshake ──────▶│                                 │
  │◀─── FW cert (signed by   │                                 │
  │     intermediate CA) ────│                                 │
  │                          │                                 │
  │==== Encrypted session ===│                                 │
  │     (client ↔ firewall)  │                                 │
  │                          │──── New TLS handshake ─────────▶│
  │                          │◀─── Server cert ────────────────│
  │                          │==== Encrypted session ==========│
  │                          │     (firewall ↔ backend)        │
  │                          │                                 │
  │     Firewall decrypts,   │                                 │
  │     inspects with IDPS,  │                                 │
  │     re-encrypts          │                                 │

Docs: Azure Firewall Premium TLS inspection

3.4 Create the Azure Firewall (Premium)

az network firewall create \
  --name fw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --sku AZFW_VNet \
  --tier Premium \
  --vnet-name vnet-hub-eastus \
  --public-ip fw-pip-eastus \
  --firewall-policy fw-policy-zerotrust

Docs: Deploy Azure Firewall Premium — CLI

3.5 Configure Network Rules (Zero Trust: Deny All by Default)

Azure Firewall uses deny-all by default. You must explicitly allow traffic:

# Create a rule collection group
az network firewall policy rule-collection-group create \
  --name DefaultNetworkRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --priority 200

# Allow DNS traffic to Azure DNS only
az network firewall policy rule-collection-group collection add-filter-collection \
  --rule-collection-group-name DefaultNetworkRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name AllowDNS \
  --collection-priority 100 \
  --rule-type NetworkRule \
  --action Allow \
  --rule-name AllowAzureDNS \
  --source-addresses "10.0.0.0/16" \
  --destination-addresses "168.63.129.16" \
  --destination-ports 53 \
  --ip-protocols UDP TCP

# Allow HTTPS to specific backend subnets only
az network firewall policy rule-collection-group collection add-filter-collection \
  --rule-collection-group-name DefaultNetworkRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name AllowAppTraffic \
  --collection-priority 200 \
  --rule-type NetworkRule \
  --action Allow \
  --rule-name AllowHTTPS \
  --source-addresses "10.0.1.0/24" \
  --destination-addresses "10.0.10.0/24" "10.0.11.0/24" \
  --destination-ports 443 \
  --ip-protocols TCP

3.6 Configure Application Rules (FQDN Filtering)

Application rules allow outbound traffic only to approved FQDNs:

# Create application rule collection group
az network firewall policy rule-collection-group create \
  --name DefaultApplicationRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --priority 300

# Allow outbound to Microsoft services only
az network firewall policy rule-collection-group collection add-filter-collection \
  --rule-collection-group-name DefaultApplicationRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name AllowMicrosoft \
  --collection-priority 100 \
  --rule-type ApplicationRule \
  --action Allow \
  --rule-name AllowAzureServices \
  --source-addresses "10.0.0.0/16" \
  --protocols Https=443 \
  --fqdn-tags AzureKubernetesService WindowsUpdate

# Allow specific external FQDNs
az network firewall policy rule-collection-group collection add-filter-collection \
  --rule-collection-group-name DefaultApplicationRuleGroup \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --name AllowSpecificFQDNs \
  --collection-priority 200 \
  --rule-type ApplicationRule \
  --action Allow \
  --rule-name AllowGitHub \
  --source-addresses "10.0.0.0/16" \
  --protocols Https=443 \
  --target-fqdns "github.com" "*.github.com" "*.githubusercontent.com"

3.7 Configure IDPS (Premium — Zero Trust: Assume Breach)

The Intrusion Detection and Prevention System inspects traffic for known attack signatures:

# Set IDPS to Alert and Deny mode
az network firewall policy intrusion-detection add \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --mode "Alert and Deny"

# Add signature overrides for critical threats
# (set high-severity signatures to Deny)
az network firewall policy intrusion-detection add \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --signature-id 2024897 \
  --mode Deny

# Bypass IDPS for trusted internal traffic (optional, use sparingly)
az network firewall policy intrusion-detection add \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network \
  --bypass-rule-name TrustedMonitoring \
  --protocol TCP \
  --source-addresses "10.0.50.0/24" \
  --destination-addresses "10.0.60.0/24" \
  --destination-ports 9090

Docs: Azure Firewall IDPS

3.8 Route Tables — Force Traffic Through Firewall

All spoke VNet traffic must route through the firewall. Create User Defined Routes (UDRs):

# Get the firewall private IP
FW_PRIVATE_IP=$(az network firewall show \
  --name fw-zerotrust-eastus \
  --resource-group rg-zerotrust-network \
  --query "ipConfigurations[0].privateIPAddress" -o tsv)

# Create route table for spoke subnets
az network route-table create \
  --name rt-spoke-to-firewall \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --disable-bgp-route-propagation true

# Default route → firewall
az network route-table route create \
  --name default-to-firewall \
  --route-table-name rt-spoke-to-firewall \
  --resource-group rg-zerotrust-network \
  --address-prefix 0.0.0.0/0 \
  --next-hop-type VirtualAppliance \
  --next-hop-ip-address $FW_PRIVATE_IP

# Associate route table with spoke subnets
az network vnet subnet update \
  --name snet-workloads \
  --vnet-name vnet-spoke-1 \
  --resource-group rg-zerotrust-network \
  --route-table rt-spoke-to-firewall

3.9 Azure Firewall Zero Trust Checklist

  • Premium SKU deployed for TLS inspection and IDPS
  • Deny-all default — only explicit allow rules exist
  • TLS inspection enabled with intermediate CA from Key Vault
  • IDPS set to Alert and Deny mode
  • Threat Intelligence set to Deny mode (not just Alert)
  • Application rules restrict outbound to approved FQDNs only
  • Network rules use specific IP ranges, not *
  • UDRs force all spoke traffic through the firewall
  • No direct internet access from spoke VNets (all traffic via firewall)
  • Diagnostic logs sent to Log Analytics workspace
  • Firewall Policy used (not classic rules) for centralized management
  • DNS proxy enabled on the firewall for FQDN resolution in network rules

Part 4: Integrating All Services Together

This section covers wiring Traffic Manager, Application Gateway, and Azure Firewall Premium into a cohesive Zero Trust architecture using hub-and-spoke networking.

Official Docs: Hub-spoke network topology

Zero Trust Reference: Secure networks with Zero Trust

4.1 Hub-and-Spoke VNet Architecture

The recommended pattern places the firewall and Application Gateway in the hub VNet, with workloads in peered spoke VNets:

┌──────────────────────────────── Hub VNet (10.0.0.0/16) ────────────────────────────────┐
│                                                                                         │
│   ┌─────────────────────┐    ┌──────────────────────┐    ┌────────────────────┐         │
│   │  AzureFirewallSubnet│    │  snet-appgw           │    │  GatewaySubnet     │         │
│   │  10.0.2.0/26        │    │  10.0.1.0/24          │    │  10.0.3.0/27       │         │
│   │                     │    │                       │    │                    │         │
│   │  ┌───────────────┐  │    │  ┌─────────────────┐  │    │  (VPN/ExpressRoute │         │
│   │  │ Azure Firewall│  │    │  │ App Gateway      │  │    │   if needed)       │         │
│   │  │    Premium     │  │    │  │ + WAF v2         │  │    │                    │         │
│   │  └───────────────┘  │    │  └─────────────────┘  │    └────────────────────┘         │
│   └─────────────────────┘    └──────────────────────┘                                   │
│                                                                                         │
└─────────────────────┬──────────────────────────────────────┬────────────────────────────┘
                      │ VNet Peering                         │ VNet Peering
                      │                                      │
    ┌─────────────────▼──────────────┐     ┌─────────────────▼──────────────┐
    │   Spoke VNet 1 (10.0.10.0/24) │     │   Spoke VNet 2 (10.0.11.0/24) │
    │                                │     │                                │
    │   ┌──────────────────────┐     │     │   ┌──────────────────────┐     │
    │   │  App Service / AKS   │     │     │   │  VMs / Databases     │     │
    │   │  (Private Endpoints) │     │     │   │  (Private Endpoints) │     │
    │   └──────────────────────┘     │     │   └──────────────────────┘     │
    │                                │     │                                │
    │   UDR: 0.0.0.0/0 → Firewall   │     │   UDR: 0.0.0.0/0 → Firewall   │
    └────────────────────────────────┘     └────────────────────────────────┘

4.2 VNet Peering Setup

# Create spoke VNets
az network vnet create \
  --name vnet-spoke-1 \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --address-prefixes 10.0.10.0/24

az network vnet create \
  --name vnet-spoke-2 \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --address-prefixes 10.0.11.0/24

# Peer hub → spoke-1
az network vnet peering create \
  --name hub-to-spoke1 \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-hub-eastus \
  --remote-vnet vnet-spoke-1 \
  --allow-forwarded-traffic true \
  --allow-gateway-transit true

# Peer spoke-1 → hub
az network vnet peering create \
  --name spoke1-to-hub \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-spoke-1 \
  --remote-vnet vnet-hub-eastus \
  --allow-forwarded-traffic true \
  --use-remote-gateways false

# Repeat for spoke-2
az network vnet peering create \
  --name hub-to-spoke2 \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-hub-eastus \
  --remote-vnet vnet-spoke-2 \
  --allow-forwarded-traffic true \
  --allow-gateway-transit true

az network vnet peering create \
  --name spoke2-to-hub \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-spoke-2 \
  --remote-vnet vnet-hub-eastus \
  --allow-forwarded-traffic true \
  --use-remote-gateways false

4.3 End-to-End Traffic Flow

Here is exactly how a request flows through the entire Zero Trust stack:

Step 1: DNS Resolution
  User browser ──DNS query──▶ Azure Traffic Manager
                              │
                              ├─ Checks health probes for all endpoints
                              ├─ Selects endpoint based on routing method
                              └─ Returns IP of Application Gateway (Region A)

Step 2: Layer 7 Inspection (Application Gateway + WAF)
  User browser ──HTTPS──▶ Application Gateway public IP
                          │
                          ├─ TLS termination (decrypts request)
                          ├─ WAF inspection (OWASP 3.2 rules)
                          ├─ Bot detection (Microsoft Bot Manager)
                          ├─ Rate limit check (custom rules)
                          ├─ Path-based routing decision
                          └─ Re-encrypts → sends to Azure Firewall

Step 3: Network Inspection (Azure Firewall Premium)
  App Gateway ──HTTPS──▶ Azure Firewall (via UDR)
                         │
                         ├─ TLS inspection (decrypt with intermediate CA)
                         ├─ IDPS signature matching
                         ├─ Network rule evaluation (IP/port allow list)
                         ├─ Application rule evaluation (FQDN filtering)
                         ├─ Threat intelligence check
                         └─ Re-encrypts → forwards to backend

Step 4: Backend Processing
  Azure Firewall ──HTTPS──▶ Backend in Spoke VNet
                            │
                            ├─ Private endpoint (no public IP)
                            ├─ NSG on subnet (additional micro-segmentation)
                            └─ Application processes request

4.4 Private Endpoints for Backend Services

Zero Trust requires that backend services have no public internet exposure:

# Create private endpoint for Azure SQL
az network private-endpoint create \
  --name pe-sql-zerotrust \
  --resource-group rg-zerotrust-network \
  --vnet-name vnet-spoke-1 \
  --subnet snet-workloads \
  --private-connection-resource-id /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{sql-server-name} \
  --group-id sqlServer \
  --connection-name sql-private-connection

# Create private DNS zone for SQL
az network private-dns zone create \
  --name privatelink.database.windows.net \
  --resource-group rg-zerotrust-network

# Link private DNS zone to hub VNet
az network private-dns link vnet create \
  --name link-hub \
  --resource-group rg-zerotrust-network \
  --zone-name privatelink.database.windows.net \
  --virtual-network vnet-hub-eastus \
  --registration-enabled false

Docs: Private endpoints

4.5 Network Security Groups (Micro-segmentation)

NSGs provide an additional layer of micro-segmentation within each subnet:

# Create NSG for workload subnet
az network nsg create \
  --name nsg-workloads \
  --resource-group rg-zerotrust-network \
  --location eastus

# Allow inbound HTTPS from Application Gateway subnet only
az network nsg rule create \
  --nsg-name nsg-workloads \
  --resource-group rg-zerotrust-network \
  --name AllowAppGateway \
  --priority 100 \
  --source-address-prefixes 10.0.1.0/24 \
  --destination-port-ranges 443 \
  --protocol TCP \
  --access Allow \
  --direction Inbound

# Deny all other inbound traffic
az network nsg rule create \
  --nsg-name nsg-workloads \
  --resource-group rg-zerotrust-network \
  --name DenyAllInbound \
  --priority 4096 \
  --source-address-prefixes "*" \
  --destination-port-ranges "*" \
  --protocol "*" \
  --access Deny \
  --direction Inbound

# Associate NSG with subnet
az network vnet subnet update \
  --name snet-workloads \
  --vnet-name vnet-spoke-1 \
  --resource-group rg-zerotrust-network \
  --network-security-group nsg-workloads

Part 5: Monitoring, Logging, and Verification

Zero Trust requires continuous monitoring. Every component must log to a central workspace.

Official Docs: Azure Monitor overview

5.1 Create Log Analytics Workspace

az monitor log-analytics workspace create \
  --workspace-name law-zerotrust \
  --resource-group rg-zerotrust-network \
  --location eastus \
  --retention-time 90

5.2 Enable Diagnostics on All Services

# Get workspace ID
WORKSPACE_ID=$(az monitor log-analytics workspace show \
  --workspace-name law-zerotrust \
  --resource-group rg-zerotrust-network \
  --query id -o tsv)

# Azure Firewall diagnostics
az monitor diagnostic-settings create \
  --name fw-diagnostics \
  --resource /subscriptions/{sub-id}/resourceGroups/rg-zerotrust-network/providers/Microsoft.Network/azureFirewalls/fw-zerotrust-eastus \
  --workspace $WORKSPACE_ID \
  --logs '[
    {"category": "AzureFirewallApplicationRule", "enabled": true},
    {"category": "AzureFirewallNetworkRule", "enabled": true},
    {"category": "AzureFirewallDnsProxy", "enabled": true},
    {"category": "AZFWThreatIntel", "enabled": true},
    {"category": "AZFWIdpsSignature", "enabled": true}
  ]' \
  --metrics '[{"category": "AllMetrics", "enabled": true}]'

# Application Gateway diagnostics (see Part 2, section 2.7)

# Traffic Manager diagnostics (see Part 1, section 1.5)

5.3 KQL Query Reference — Day-to-Day Operations

These queries are designed for daily monitoring, health checks, and operational awareness. Run them in Log Analytics against the workspace receiving your diagnostic logs.

Official Docs: KQL overview · Azure Firewall log schema · Application Gateway log schema


5.3.1 Azure Firewall — Daily Operations

Denied traffic summary (last 24 hours) — check deny-all posture is working:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| parse msg_s with Protocol " request from " SourceIP ":" SourcePort " to " DestIP ":" DestPort ". Action: " Action "." *
| summarize
    DenyCount = count(),
    UniqueSourceIPs = dcount(SourceIP)
    by bin(TimeGenerated, 1h)
| render timechart

Top 20 denied source IPs — spot repeat offenders:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| parse msg_s with * " request from " SourceIP ":" * " to " DestIP ":" DestPort ". Action: " Action "." *
| summarize DenyCount = count() by SourceIP
| top 20 by DenyCount desc

Top allowed outbound FQDNs — verify expected outbound traffic:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AzureFirewallApplicationRule"
| where msg_s contains "Allow"
| parse msg_s with * " to " FQDN ":" * ". Action: Allow." *
| summarize RequestCount = count() by FQDN
| top 30 by RequestCount desc

Firewall throughput by hour — capacity planning:

AzureMetrics
| where TimeGenerated > ago(24h)
| where ResourceType == "AZUREFIREWALLS"
| where MetricName == "Throughput"
| summarize AvgThroughputBps = avg(Average) by bin(TimeGenerated, 1h)
| extend AvgThroughputMbps = round(AvgThroughputBps / 1000000, 2)
| project TimeGenerated, AvgThroughputMbps
| render timechart

SNAT port utilisation — watch for exhaustion (>90% is critical):

AzureMetrics
| where TimeGenerated > ago(24h)
| where ResourceType == "AZUREFIREWALLS"
| where MetricName == "SNATPortUtilization"
| summarize MaxUtilization = max(Maximum), AvgUtilization = avg(Average) by bin(TimeGenerated, 15m)
| render timechart

DNS proxy queries — see what your workloads are resolving:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AzureFirewallDnsProxy"
| parse msg_s with "DNS Request: " ClientIP ":" ClientPort " - " QueryID " " RequestType " " RequestClass " " FQDN ". " Protocol " " SourceSize " " * "NOERROR" *
| summarize QueryCount = count() by FQDN
| top 30 by QueryCount desc

DNS failures — spot resolution problems:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AzureFirewallDnsProxy"
| where msg_s !contains "NOERROR"
| parse msg_s with "DNS Request: " ClientIP ":" ClientPort " - " QueryID " " RequestType " " RequestClass " " FQDN ". " Protocol " " SourceSize " " * ResponseCode *
| summarize FailCount = count() by FQDN, ResponseCode
| order by FailCount desc

5.3.2 Azure Firewall Premium — Threat Detection

IDPS alerts (last 24 hours) — all threat detections:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWIdpsSignature"
| parse msg_s with * "SignatureId: " SignatureId ". " *
| project
    TimeGenerated,
    SignatureId,
    msg_s,
    SourceIP = column_ifexists("SourceIP", ""),
    DestinationIP = column_ifexists("DestinationIP", ""),
    Action = column_ifexists("Action", "")
| order by TimeGenerated desc

IDPS threat summary by severity — daily security posture:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWIdpsSignature"
| parse msg_s with * "Severity: " Severity ". " * "SignatureId: " SignatureId ". " *
| summarize AlertCount = count() by Severity, SignatureId
| order by Severity asc, AlertCount desc

TLS inspection stats — confirm encrypted traffic is being inspected:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWTlsInspection"
| summarize
    InspectedCount = countif(msg_s contains "inspected"),
    BypassedCount = countif(msg_s contains "bypass"),
    FailedCount = countif(msg_s contains "fail")
    by bin(TimeGenerated, 1h)
| render timechart

Threat intelligence hits — traffic to known-bad IPs/domains:

AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "AZFWThreatIntel"
| project TimeGenerated, msg_s
| parse msg_s with * " from " SourceIP ":" SourcePort " to " DestIP ":" DestPort ". Action: " Action ". ThreatIntel: " ThreatDescription
| summarize HitCount = count() by DestIP, ThreatDescription, Action
| order by HitCount desc

5.3.3 Application Gateway + WAF — Daily Operations

Request volume and response codes (last 24 hours):

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| summarize
    TotalRequests = count(),
    Http2xx = countif(httpStatus_d >= 200 and httpStatus_d < 300),
    Http3xx = countif(httpStatus_d >= 300 and httpStatus_d < 400),
    Http4xx = countif(httpStatus_d >= 400 and httpStatus_d < 500),
    Http5xx = countif(httpStatus_d >= 500)
    by bin(TimeGenerated, 1h)
| render timechart

Backend health and response times:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| summarize
    AvgLatencyMs = avg(timeTaken_d * 1000),
    P95LatencyMs = percentile(timeTaken_d * 1000, 95),
    P99LatencyMs = percentile(timeTaken_d * 1000, 99),
    RequestCount = count()
    by bin(TimeGenerated, 15m), serverRouted_s
| order by TimeGenerated desc

Top requested URLs — understand traffic patterns:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| summarize RequestCount = count() by requestUri_s
| top 30 by RequestCount desc

WAF blocks summary — what is the WAF catching:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize BlockCount = count() by ruleId_s, ruleGroup_s, message_s
| order by BlockCount desc
| take 30

WAF blocks by client IP — spot attack sources:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize
    BlockCount = count(),
    Rules = make_set(ruleId_s)
    by clientIp_s
| order by BlockCount desc
| take 20

WAF matched but allowed (Detection mode) — tune before switching to Prevention:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Matched"
| summarize MatchCount = count() by ruleId_s, ruleGroup_s, message_s
| order by MatchCount desc

Failed backend connections — backends refusing or timing out:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d >= 502 and httpStatus_d <= 504
| project TimeGenerated, serverRouted_s, httpStatus_d, timeTaken_d, requestUri_s, clientIP_s
| order by TimeGenerated desc

5.3.4 Traffic Manager — Health and Routing

Endpoint health status over time:

AzureDiagnostics
| where TimeGenerated > ago(7d)
| where ResourceType == "TRAFFICMANAGERPROFILES"
| where Category == "ProbeHealthStatusEvents"
| project TimeGenerated, EndpointName_s, Status_s
| order by TimeGenerated desc

Endpoint state transitions — when did failovers happen:

AzureDiagnostics
| where TimeGenerated > ago(7d)
| where ResourceType == "TRAFFICMANAGERPROFILES"
| where Category == "ProbeHealthStatusEvents"
| sort by TimeGenerated asc
| extend PreviousStatus = prev(Status_s), PreviousEndpoint = prev(EndpointName_s)
| where EndpointName_s == PreviousEndpoint and Status_s != PreviousStatus
| project TimeGenerated, EndpointName_s, FromStatus = PreviousStatus, ToStatus = Status_s
| order by TimeGenerated desc

Endpoints currently degraded or offline:

AzureDiagnostics
| where TimeGenerated > ago(1h)
| where ResourceType == "TRAFFICMANAGERPROFILES"
| where Category == "ProbeHealthStatusEvents"
| where Status_s != "Online"
| summarize LastSeen = max(TimeGenerated) by EndpointName_s, Status_s
| order by LastSeen desc

5.4 KQL Query Reference — Fault Finding and Incident Response

These queries help you diagnose specific problems when something goes wrong. Organised by symptom.


5.4.1 "Users can't reach the site"

Step 1 — Is Traffic Manager returning healthy endpoints?

AzureDiagnostics
| where TimeGenerated > ago(1h)
| where ResourceType == "TRAFFICMANAGERPROFILES"
| where Category == "ProbeHealthStatusEvents"
| summarize arg_max(TimeGenerated, *) by EndpointName_s
| project TimeGenerated, EndpointName_s, Status_s

Step 2 — Is Application Gateway receiving requests?

AzureDiagnostics
| where TimeGenerated > ago(1h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| summarize RequestCount = count() by bin(TimeGenerated, 5m)
| render timechart

Step 3 — Is the firewall dropping legitimate traffic?

AzureDiagnostics
| where TimeGenerated > ago(1h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| parse msg_s with * " from " SourceIP ":" * " to " DestIP ":" DestPort ". Action: Deny." *
| where SourceIP startswith "10.0.1."  // Application Gateway subnet
| project TimeGenerated, SourceIP, DestIP, DestPort, msg_s
| order by TimeGenerated desc

5.4.2 "Site is slow"

Application Gateway latency breakdown — where is the time spent?

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| extend
    TotalMs = timeTaken_d * 1000
| summarize
    AvgTotal = round(avg(TotalMs), 1),
    P50 = round(percentile(TotalMs, 50), 1),
    P95 = round(percentile(TotalMs, 95), 1),
    P99 = round(percentile(TotalMs, 99), 1),
    MaxMs = round(max(TotalMs), 1)
    by bin(TimeGenerated, 15m)
| render timechart

Slowest requests — find the problem URLs:

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| extend TotalMs = timeTaken_d * 1000
| top 50 by TotalMs desc
| project TimeGenerated, requestUri_s, serverRouted_s, httpStatus_d, TotalMs, clientIP_s

Slow backends — which backend pool is the bottleneck?

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| extend TotalMs = timeTaken_d * 1000
| summarize
    AvgMs = round(avg(TotalMs), 1),
    P95Ms = round(percentile(TotalMs, 95), 1),
    Requests = count()
    by serverRouted_s
| order by P95Ms desc

Firewall processing latency — is the firewall adding delay?

AzureMetrics
| where TimeGenerated > ago(4h)
| where ResourceType == "AZUREFIREWALLS"
| where MetricName == "FirewallLatencyInMs"
| summarize
    AvgLatency = round(avg(Average), 1),
    MaxLatency = round(max(Maximum), 1)
    by bin(TimeGenerated, 5m)
| render timechart

5.4.3 "Getting 502/503/504 errors"

502 Bad Gateway — backend is unreachable or returning invalid responses:

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d == 502
| summarize
    Count502 = count()
    by bin(TimeGenerated, 5m), serverRouted_s
| render timechart

503 Service Unavailable — no healthy backends in the pool:

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d == 503
| project TimeGenerated, requestUri_s, serverRouted_s, serverStatus_s, timeTaken_d
| order by TimeGenerated desc

504 Gateway Timeout — backend took too long to respond:

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d == 504
| project TimeGenerated, requestUri_s, serverRouted_s, timeTaken_d, clientIP_s
| order by TimeGenerated desc
| take 50

Correlate 5xx errors with firewall denies at the same time:

let errors = AzureDiagnostics
| where TimeGenerated > ago(4h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where httpStatus_d >= 500
| summarize ErrorCount = count() by bin(TimeGenerated, 5m);
let denies = AzureDiagnostics
| where TimeGenerated > ago(4h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| summarize DenyCount = count() by bin(TimeGenerated, 5m);
errors
| join kind=fullouter denies on TimeGenerated
| project TimeGenerated = coalesce(TimeGenerated, TimeGenerated1), ErrorCount = coalesce(ErrorCount, 0), DenyCount = coalesce(DenyCount, 0)
| order by TimeGenerated desc
| render timechart

5.4.4 "WAF is blocking legitimate traffic" (False Positives)

Find the rule that's causing false positives:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize
    BlockCount = count(),
    SampleURIs = make_set(requestUri_s, 5),
    SampleIPs = make_set(clientIp_s, 5)
    by ruleId_s, ruleGroup_s, message_s
| order by BlockCount desc

Inspect a specific rule's blocks in detail:

// Replace RULE_ID with the rule from the query above
let targetRule = "942130";  // Example: SQL injection rule
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where ruleId_s == targetRule
| project TimeGenerated, clientIp_s, requestUri_s, details_message_s, details_data_s, action_s
| order by TimeGenerated desc
| take 50

Compare blocked vs allowed traffic for a specific URI path:

let targetPath = "/api/";
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where requestUri_s contains targetPath
| summarize Count = count() by action_s, ruleId_s
| order by Count desc

5.4.5 "Firewall is blocking something it shouldn't"

Recent denies from a specific source IP or subnet:

// Replace with the source IP/subnet you're investigating
let sourceSubnet = "10.0.10.";
AzureDiagnostics
| where TimeGenerated > ago(4h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| where msg_s contains sourceSubnet
| parse msg_s with Protocol " request from " SourceIP ":" SourcePort " to " DestIP ":" DestPort ". Action: Deny." *
| project TimeGenerated, Protocol, SourceIP, SourcePort, DestIP, DestPort
| order by TimeGenerated desc
| take 100

Denied FQDN requests — workload trying to reach a domain not in allow list:

AzureDiagnostics
| where TimeGenerated > ago(4h)
| where Category == "AzureFirewallApplicationRule"
| where msg_s contains "Deny"
| parse msg_s with * " request from " SourceIP ":" SourcePort " to " FQDN ":" DestPort *
| summarize DenyCount = count() by FQDN, SourceIP
| order by DenyCount desc

Check if a specific destination was allowed or denied:

// Replace with the destination you're troubleshooting
let targetDest = "api.example.com";
AzureDiagnostics
| where TimeGenerated > ago(4h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains targetDest
| project TimeGenerated, Category, msg_s
| order by TimeGenerated desc

IDPS false positive investigation — find what triggered a signature:

// Replace with the signature ID from your IDPS alert
let sigId = "2024897";
AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "AZFWIdpsSignature"
| where msg_s contains sigId
| project TimeGenerated, msg_s
| order by TimeGenerated desc

5.4.6 "TLS inspection is breaking something"

TLS inspection failures — certificate errors or handshake failures:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWTlsInspection"
| where msg_s contains "fail" or msg_s contains "error"
| project TimeGenerated, msg_s
| order by TimeGenerated desc

TLS bypassed connections — traffic skipping inspection:

AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWTlsInspection"
| where msg_s contains "bypass"
| parse msg_s with * "to " DestFQDN ":" *
| summarize BypassCount = count() by DestFQDN
| order by BypassCount desc

5.4.7 "Need to audit who changed what" (Change Tracking)

Firewall rule changes in the last 7 days:

AzureActivity
| where TimeGenerated > ago(7d)
| where ResourceProviderValue == "MICROSOFT.NETWORK"
| where OperationNameValue contains "firewallPolicies" or OperationNameValue contains "azureFirewalls"
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, OperationNameValue, ResourceGroup, Properties_d
| order by TimeGenerated desc

Application Gateway configuration changes:

AzureActivity
| where TimeGenerated > ago(7d)
| where ResourceProviderValue == "MICROSOFT.NETWORK"
| where OperationNameValue contains "applicationGateways"
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, OperationNameValue, ResourceGroup
| order by TimeGenerated desc

NSG rule changes — someone modified micro-segmentation:

AzureActivity
| where TimeGenerated > ago(7d)
| where ResourceProviderValue == "MICROSOFT.NETWORK"
| where OperationNameValue contains "networkSecurityGroups"
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, OperationNameValue, ResourceGroup
| order by TimeGenerated desc

5.4.8 Cross-Service Correlation Queries

End-to-end request tracing — correlate App Gateway, Firewall, and backend:

// Find requests that passed App Gateway but were denied by Firewall
let appGwRequests = AzureDiagnostics
| where TimeGenerated > ago(1h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d >= 500
| project AppGwTime = TimeGenerated, clientIP_s, requestUri_s, serverRouted_s, httpStatus_d;
let fwDenies = AzureDiagnostics
| where TimeGenerated > ago(1h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| parse msg_s with * " from " FwSourceIP ":" * " to " FwDestIP ":" FwDestPort *
| project FwTime = TimeGenerated, FwSourceIP, FwDestIP, FwDestPort;
appGwRequests
| join kind=inner (fwDenies) on $left.serverRouted_s == $right.FwDestIP
| where abs(datetime_diff('second', AppGwTime, FwTime)) < 5
| project AppGwTime, clientIP_s, requestUri_s, httpStatus_d, FwDestIP, FwDestPort

Security summary dashboard — single pane of glass for the last 24 hours:

let fw_denies = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| count | extend Metric = "Firewall Denies";
let fw_idps = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWIdpsSignature"
| count | extend Metric = "IDPS Alerts";
let fw_threat_intel = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "AZFWThreatIntel"
| count | extend Metric = "Threat Intel Hits";
let waf_blocks = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| count | extend Metric = "WAF Blocks";
let total_requests = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| count | extend Metric = "Total Requests";
let error_requests = AzureDiagnostics
| where TimeGenerated > ago(24h)
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d >= 500
| count | extend Metric = "5xx Errors";
union fw_denies, fw_idps, fw_threat_intel, waf_blocks, total_requests, error_requests
| project Metric, Count

Anomaly detection — sudden spike in denied traffic (possible attack):

AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| where msg_s contains "Deny"
| summarize DenyCount = count() by bin(TimeGenerated, 1h)
| extend AvgDenies = avg_if(DenyCount, TimeGenerated < ago(1d))
| extend StdDevDenies = stdev_if(DenyCount, TimeGenerated < ago(1d))
| where TimeGenerated > ago(1d)
| where DenyCount > AvgDenies + (3 * StdDevDenies)
| project TimeGenerated, DenyCount, AvgDenies, StdDevDenies
| order by TimeGenerated desc

5.4.9 NSG Flow Log Queries

If you have NSG flow logs enabled (recommended), use these to trace network-level connectivity:

Denied flows in spoke subnets — micro-segmentation working:

AzureNetworkAnalytics_CL
| where TimeGenerated > ago(4h)
| where FlowStatus_s == "D"  // Denied
| summarize DeniedFlows = count() by NSGRule_s, SrcIP_s, DestIP_s, DestPort_d
| order by DeniedFlows desc
| take 30

Traffic between spokes — should be zero in strict Zero Trust:

AzureNetworkAnalytics_CL
| where TimeGenerated > ago(24h)
| where SrcIP_s startswith "10.0.10." and DestIP_s startswith "10.0.11."
   or SrcIP_s startswith "10.0.11." and DestIP_s startswith "10.0.10."
| summarize FlowCount = count() by SrcIP_s, DestIP_s, DestPort_d, FlowStatus_s
| order by FlowCount desc

Docs: NSG flow log schema

5.4 Azure Monitor Alerts

Set up alerts for critical Zero Trust events:

# Alert: Firewall IDPS detected a threat
az monitor metrics alert create \
  --name alert-fw-idps-threat \
  --resource-group rg-zerotrust-network \
  --scopes /subscriptions/{sub-id}/resourceGroups/rg-zerotrust-network/providers/Microsoft.Network/azureFirewalls/fw-zerotrust-eastus \
  --condition "total IDPS Signature Count > 0" \
  --window-size 5m \
  --evaluation-frequency 1m \
  --severity 1 \
  --action-group /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Insights/actionGroups/ag-security-team

# Alert: WAF blocked more than 100 requests in 5 minutes
az monitor metrics alert create \
  --name alert-waf-blocks \
  --resource-group rg-zerotrust-network \
  --scopes /subscriptions/{sub-id}/resourceGroups/rg-zerotrust-network/providers/Microsoft.Network/applicationGateways/appgw-zerotrust-eastus \
  --condition "total BlockedReqCount > 100" \
  --window-size 5m \
  --evaluation-frequency 1m \
  --severity 2 \
  --action-group /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Insights/actionGroups/ag-security-team

# Alert: Traffic Manager endpoint went offline
az monitor activity-log alert create \
  --name alert-tm-endpoint-down \
  --resource-group rg-zerotrust-network \
  --condition category=Administrative and operationName=Microsoft.Network/trafficManagerProfiles/endpoints/write \
  --action-group /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.Insights/actionGroups/ag-security-team

5.5 Microsoft Defender for Cloud Integration

Enable Defender for Cloud to get continuous security assessments of your Zero Trust deployment:

# Enable Defender for Network Security
az security pricing create \
  --name VirtualMachines \
  --tier Standard

az security pricing create \
  --name SqlServers \
  --tier Standard

az security pricing create \
  --name AppServices \
  --tier Standard

Docs: Microsoft Defender for Cloud


Part 6: Verification and Testing

6.1 Verification Checklist

Run through this checklist to verify your Zero Trust deployment:

Traffic Manager:

  • DNS resolves zt-traffic-manager.trafficmanager.net to the correct Application Gateway IP
  • Disabling primary endpoint causes failover to secondary within TTL window
  • Health probe logs appear in Log Analytics

Application Gateway + WAF:

  • HTTPS requests reach the Application Gateway and are processed
  • HTTP requests are redirected to HTTPS (or blocked)
  • WAF blocks a test SQL injection: curl "https://yourdomain.com/?id=1' OR '1'='1"
  • WAF blocks a test XSS: curl "https://yourdomain.com/?q=<script>alert(1)</script>"
  • Path-based routing sends /api/* to the API pool and /* to the web pool
  • WAF logs show both allowed and blocked requests in Log Analytics

Azure Firewall Premium:

  • Traffic from spoke VNets routes through the firewall (check effective routes)
  • Denied traffic appears in firewall logs
  • IDPS alerts fire for test traffic (use EICAR test file for safe testing)
  • TLS inspection is active (check AZFWTlsInspection logs)
  • Only approved FQDNs are reachable from spoke VNets
  • Threat intelligence blocks known-bad test domains

Connectivity:

  • End-to-end: browser → Traffic Manager → App Gateway → Firewall → Backend works
  • Backend services have no public IP (only reachable via private endpoints)
  • NSGs on spoke subnets block all traffic except from the hub

6.2 Testing Commands

# Verify Traffic Manager DNS resolution
nslookup zt-traffic-manager.trafficmanager.net

# Verify Application Gateway health
az network application-gateway show-backend-health \
  --name appgw-zerotrust-eastus \
  --resource-group rg-zerotrust-network

# Check effective routes on a spoke VM NIC (should show 0.0.0.0/0 → firewall)
az network nic show-effective-route-table \
  --name {nic-name} \
  --resource-group rg-zerotrust-network

# Test WAF with a SQL injection attempt
curl -k -o /dev/null -w "%{http_code}" \
  "https://yourdomain.com/?id=1%27%20OR%20%271%27%3D%271"
# Expected: 403 (blocked by WAF)

# Check firewall rule hit counts
az network firewall policy rule-collection-group list \
  --policy-name fw-policy-zerotrust \
  --resource-group rg-zerotrust-network

Quick Reference: Key Azure Docs Links

ServiceOverviewZero Trust GuidePricing

Additional Resources: