Application Gateway Rules in a Zero Trust Architecture
Overview
Adding a new routing rule to an Application Gateway in a Zero Trust architecture is more than just configuring a listener and backend pool. Every component — DNS resolution, network security groups, private endpoints, and certificate validation — must be explicitly verified and locked down.
This guide walks through the complete process of adding an Application Gateway rule in a Zero Trust setup, including Private DNS zones (with legacy Active Directory considerations), NSG rules, and a full verification checklist.
Zero Trust Principle: "Never trust, always verify." Every network flow must be explicitly permitted, authenticated, and encrypted. No implicit trust based on network location.
Zero Trust Pillars Applied to Application Gateway
| Pillar | Application to App Gateway |
|---|---|
| Verify explicitly | Mutual TLS, certificate validation on every backend connection |
| Least privilege access | NSGs restrict traffic to only required ports and sources |
| Assume breach | WAF in Prevention mode, all traffic logged and monitored, network segmentation enforced |
Architecture — Zero Trust Network Topology
Internet
↓
[Azure DDoS Protection]
↓
[Public IP — Static, Standard SKU]
↓
┌──────────────────────────────────────────────────────────┐
│ Application Gateway Subnet (10.0.1.0/24) │
│ NSG: Allow 443/80 inbound from Internet │
│ Allow GatewayManager inbound (65200-65535) │
│ Deny all other inbound │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Application Gateway (WAF_v2) │ │
│ │ - Frontend: Public + Private IP │ │
│ │ - WAF Policy: Prevention mode │ │
│ │ - SSL Policy: TLS 1.2+ │ │
│ │ - End-to-end SSL enabled │ │
│ └───────────────┬─────────────────────┘ │
└──────────────────┼───────────────────────────────────────┘
│ (Private link / VNet routing)
↓
┌──────────────────────────────────────────────────────────┐
│ Backend Subnet (10.0.2.0/24) │
│ NSG: Allow inbound ONLY from App Gateway subnet │
│ on required ports (443) │
│ Deny all other inbound │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
│ │ App Svc │ │ APIM │ │ VM / VMSS │ │
│ │ (PE) │ │ (PE) │ │ (Private IP) │ │
│ └────────────┘ └────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────┐
│ Private DNS Zones │
│ - privatelink.azurewebsites.net │
│ - privatelink.azure-api.net │
│ - Custom internal zones (e.g. internal.contoso.com) │
│ Linked to App Gateway VNet │
└──────────────────────────────────────────────────────────┘
Prerequisites Before Adding a Rule
Before creating a new routing rule, confirm the following are in place:
| Requirement | Detail |
|---|---|
| App Gateway deployed | WAF_v2 SKU with autoscaling, in a dedicated /24 subnet |
| WAF policy attached | Prevention mode with OWASP 3.2 and Bot Manager 1.1 |
| SSL certificate | In Key Vault, App Gateway has managed identity with GET secret permission |
| Diagnostic settings | Access, Performance, and Firewall logs sent to Log Analytics |
| DDoS Protection | Standard plan associated with the VNet |
| Private DNS zones | Created and linked to the App Gateway VNet |
Step 1 — Define the Backend Target
Determine what the new rule will route to:
| Property | Example Value |
|---|---|
| Backend FQDN or IP | app-orders-prod.azurewebsites.net (private endpoint) |
| Backend port | 443 |
| Backend protocol | HTTPS |
| Health probe path | /health or /api/health |
| Expected host header | app-orders-prod.azurewebsites.net |
Create the Backend Pool
// Add to backendAddressPools array
{
name: 'pool-orders'
properties: {
backendAddresses: [
{
fqdn: 'app-orders-prod.azurewebsites.net'
}
]
}
}
Zero Trust Note: Use the private endpoint FQDN, not the public FQDN. The Application Gateway must resolve this via Private DNS to the private IP, ensuring traffic never traverses the public internet.
Step 2 — Configure HTTP Settings (Backend Settings)
// Add to backendHttpSettingsCollection array
{
name: 'settings-orders'
properties: {
port: 443
protocol: 'Https'
cookieBasedAffinity: 'Disabled'
requestTimeout: 30
pickHostNameFromBackendAddress: true // Critical for App Service backends
probe: {
id: resourceId('Microsoft.Network/applicationGateways/probes', appGwName, 'probe-orders')
}
trustedRootCertificates: [] // Not needed for well-known CAs
}
}
When to Use pickHostNameFromBackendAddress vs Custom Host Header
| Scenario | Setting |
|---|---|
| App Service / APIM with private endpoint | pickHostNameFromBackendAddress: true |
| Custom domain on backend | hostName: 'orders.contoso.com' |
| VM/VMSS with internal FQDN | hostName: 'orders.internal.contoso.com' |
Critical: If the host header does not match what the backend expects, you will get
502 Bad Gatewayor404 Not Found. This is the most common misconfiguration.
Step 3 — Create a Health Probe
// Add to probes array
{
name: 'probe-orders'
properties: {
protocol: 'Https'
path: '/health'
interval: 30
timeout: 10
unhealthyThreshold: 3
pickHostNameFromBackendHttpSettings: true
match: {
statusCodes: [ '200-399' ]
}
}
}
Health Probe Best Practices in Zero Trust
- Always use HTTPS for probes — never HTTP, even internally
- Set
pickHostNameFromBackendHttpSettings: trueto match the host header - Use a dedicated health endpoint (
/health) that checks downstream dependencies - Return
200only when the application is genuinely ready to serve traffic - Set a reasonable
unhealthyThreshold(3) — too low causes flapping, too high causes slow failover
Step 4 — Add the Listener
For a New Hostname (Multi-site Listener)
// Add to httpListeners array
{
name: 'listener-orders'
properties: {
frontendIPConfiguration: {
id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', appGwName, 'appGwPublicFrontendIp')
}
frontendPort: {
id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGwName, 'port_443')
}
protocol: 'Https'
sslCertificate: {
id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appGwName, 'wildcard-cert')
}
hostName: 'orders.contoso.com' // Multi-site: route by hostname
requireServerNameIndication: true // Required for multi-site HTTPS
}
}
For an Additional Path on an Existing Listener
If adding a new path under an existing listener (e.g. /orders/*), skip creating a new listener and go to Step 5b to add a path rule instead.
Step 5 — Create the Routing Rule
5a — Basic Rule (Dedicated Listener)
// Add to requestRoutingRules array
{
name: 'rule-orders'
properties: {
priority: 300 // Must be unique across all rules
ruleType: 'Basic'
httpListener: {
id: resourceId('Microsoft.Network/applicationGateways/httpListeners', appGwName, 'listener-orders')
}
backendAddressPool: {
id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', appGwName, 'pool-orders')
}
backendHttpSettings: {
id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGwName, 'settings-orders')
}
rewriteRuleSet: {
id: resourceId('Microsoft.Network/applicationGateways/rewriteRuleSets', appGwName, 'security-headers')
}
}
}
5b — Path-Based Rule (Additional Path on Existing Listener)
// Add to the existing urlPathMaps pathRules array
{
name: 'orders-path'
properties: {
paths: [ '/orders/*' ]
backendAddressPool: {
id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', appGwName, 'pool-orders')
}
backendHttpSettings: {
id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGwName, 'settings-orders')
}
rewriteRuleSet: {
id: resourceId('Microsoft.Network/applicationGateways/rewriteRuleSets', appGwName, 'security-headers')
}
}
}
Rule Priority Guidance
| Priority Range | Purpose |
|---|---|
| 1–99 | Reserved for HTTP-to-HTTPS redirects |
| 100–199 | Primary application rules |
| 200–299 | Secondary application rules |
| 300–499 | Additional service rules |
| 500+ | Catch-all / default rules |
Important: Every rule must have a unique priority value. If two rules have the same priority, the deployment will fail.
Step 6 — Private DNS Configuration
This is the step most often missed or misconfigured in Zero Trust environments. The Application Gateway must resolve backend FQDNs to private IPs via Private DNS zones.
6a — Azure Private DNS Zones
For each Azure PaaS service accessed via private endpoint, a corresponding Private DNS zone must exist:
| Service | Private DNS Zone |
|---|---|
| App Service / Function App | privatelink.azurewebsites.net |
| API Management | privatelink.azure-api.net |
| Azure SQL | privatelink.database.windows.net |
| Storage (blob) | privatelink.blob.core.windows.net |
| Key Vault | privatelink.vaultcore.azure.net |
| Service Bus | privatelink.servicebus.windows.net |
Create the Private DNS Zone and Link
// Private DNS Zone
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.azurewebsites.net'
location: 'global'
}
// Link to App Gateway VNet
resource dnsVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
parent: privateDnsZone
name: 'link-vnet-appgw'
location: 'global'
properties: {
virtualNetwork: {
id: vnet.id
}
registrationEnabled: false // Do not auto-register VM records
}
}
Create the Private Endpoint and DNS Record
// Private Endpoint for App Service
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
name: 'pe-app-orders-prod'
location: location
properties: {
subnet: {
id: backendSubnet.id
}
privateLinkServiceConnections: [
{
name: 'plsc-app-orders'
properties: {
privateLinkServiceId: appService.id
groupIds: [ 'sites' ]
}
}
]
}
}
// Automatic DNS registration in Private DNS Zone
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
parent: privateEndpoint
name: 'default'
properties: {
privateDnsZoneConfigs: [
{
name: 'config1'
properties: {
privateDnsZoneId: privateDnsZone.id
}
}
]
}
}
After deployment, the DNS zone will contain:
app-orders-prod.azurewebsites.net → CNAME → app-orders-prod.privatelink.azurewebsites.net
app-orders-prod.privatelink.azurewebsites.net → A → 10.0.2.5
6b — Legacy Active Directory DNS Integration
In environments with an existing on-premises or Azure-hosted Active Directory Domain Services (AD DS) infrastructure, DNS resolution follows a different path. Domain-joined machines and services typically use the AD DNS servers as their primary resolver, not Azure DNS (168.63.129.16).
This means Private DNS zones will not work automatically unless you configure DNS forwarding.
┌────────────────────────────────────────────────────────────────┐
│ Legacy AD DS Environment │
│ │
│ Domain-joined servers / VMs │
│ ↓ DNS query │
│ AD DNS Server (DC01: 10.1.0.4, DC02: 10.1.0.5) │
│ ↓ Conditional forwarder │
│ Azure DNS Private Resolver (10.0.3.4) │
│ ↓ │
│ Azure Private DNS Zone (privatelink.azurewebsites.net) │
│ ↓ │
│ Resolved: 10.0.2.5 (Private Endpoint IP) │
└────────────────────────────────────────────────────────────────┘
Option A — Azure DNS Private Resolver (Recommended)
The modern approach that replaces the need for custom DNS VMs:
// DNS Private Resolver
resource dnsResolver 'Microsoft.Network/dnsResolvers@2022-07-01' = {
name: 'dnspr-${environment}'
location: location
properties: {
virtualNetwork: {
id: vnet.id
}
}
}
// Inbound Endpoint — AD DNS forwards to this
resource inboundEndpoint 'Microsoft.Network/dnsResolvers/inboundEndpoints@2022-07-01' = {
parent: dnsResolver
name: 'inbound'
location: location
properties: {
ipConfigurations: [
{
subnet: {
id: dnsResolverInboundSubnet.id // Dedicated /28 subnet minimum
}
privateIpAllocationMethod: 'Dynamic'
}
]
}
}
Then configure your AD DNS servers with a conditional forwarder:
| Setting | Value |
|---|---|
| DNS Domain | privatelink.azurewebsites.net |
| Forwarder IP | Inbound endpoint IP (e.g. 10.0.3.4) |
| DNS Domain | privatelink.azure-api.net |
| Forwarder IP | Inbound endpoint IP (e.g. 10.0.3.4) |
Repeat for every privatelink.* zone your environment uses.
On the AD DNS Server (PowerShell)
# Add conditional forwarders on each Domain Controller
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.azurewebsites.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.azure-api.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.database.windows.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.vaultcore.azure.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.blob.core.windows.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Add-DnsServerConditionalForwarderZone `
-Name "privatelink.servicebus.windows.net" `
-MasterServers 10.0.3.4 `
-ReplicationScope "Forest"
Option B — Custom DNS Forwarder VMs (Legacy Approach)
If you cannot deploy Azure DNS Private Resolver (older subscriptions or policy restrictions):
- Deploy two VMs in a dedicated subnet running DNS (Windows DNS or BIND)
- Configure them to forward
privatelink.*queries to168.63.129.16(Azure DNS) - Configure all other queries to forward to your AD DNS servers
- Set the VNet custom DNS to point to these forwarder VMs
Important: This approach requires managing VM availability, patching, and monitoring. Azure DNS Private Resolver is strongly preferred.
VNet Custom DNS Settings
If your VNet uses custom DNS (pointing to AD DNS servers), ensure the resolution chain works:
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: 'vnet-appgw-${environment}'
location: location
properties: {
addressSpace: {
addressPrefixes: [ '10.0.0.0/16' ]
}
dhcpOptions: {
dnsServers: [
'10.1.0.4' // AD DNS Server (DC01)
'10.1.0.5' // AD DNS Server (DC02)
]
}
// subnets...
}
}
Zero Trust DNS Principle: DNS resolution must always resolve private endpoint FQDNs to private IPs. If a query accidentally resolves to a public IP, the Application Gateway will attempt to route traffic over the internet, bypassing your Zero Trust network controls.
6c — Verifying DNS Resolution
From a VM in the same VNet as the Application Gateway, verify resolution:
# Should resolve to private IP (e.g. 10.0.2.5), NOT a public IP
nslookup app-orders-prod.azurewebsites.net
# Expected output:
# Name: app-orders-prod.privatelink.azurewebsites.net
# Address: 10.0.2.5
# Aliases: app-orders-prod.azurewebsites.net
# Linux equivalent
dig app-orders-prod.azurewebsites.net +short
# Expected: 10.0.2.5
If you see a public IP, the Private DNS zone is not linked to the VNet, the private endpoint DNS group is misconfigured, or the conditional forwarder is missing from the AD DNS server.
Step 7 — Network Security Group (NSG) Configuration
NSGs are the enforcement layer that makes Zero Trust tangible at the network level. Every subnet must have an NSG, and every rule must be explicitly justified.
Application Gateway Subnet NSG
The Application Gateway subnet has mandatory inbound rules required by Azure:
resource nsgAppGw 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: 'nsg-snet-appgw-${environment}'
location: location
properties: {
securityRules: [
// === MANDATORY RULES ===
{
name: 'Allow-GatewayManager'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: 'GatewayManager'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '65200-65535'
description: 'Required for Azure to manage the Application Gateway'
}
}
{
name: 'Allow-AzureLoadBalancer'
properties: {
priority: 110
direction: 'Inbound'
access: 'Allow'
protocol: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
description: 'Required for Azure health probes'
}
}
// === APPLICATION RULES ===
{
name: 'Allow-HTTPS-Inbound'
properties: {
priority: 200
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
destinationAddressPrefix: '10.0.1.0/24'
destinationPortRange: '443'
description: 'Allow HTTPS from internet to App Gateway'
}
}
{
name: 'Allow-HTTP-Inbound'
properties: {
priority: 210
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
destinationAddressPrefix: '10.0.1.0/24'
destinationPortRange: '80'
description: 'Allow HTTP for redirect to HTTPS'
}
}
// === DENY ALL ===
{
name: 'Deny-All-Inbound'
properties: {
priority: 4000
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
description: 'Deny all other inbound traffic'
}
}
]
}
}
Warning: If you omit the
GatewayManagerrule on ports 65200-65535, the Application Gateway will enter a failed state and stop processing traffic. This is the most common NSG mistake.
Backend Subnet NSG
The backend subnet should only allow traffic from the Application Gateway subnet:
resource nsgBackend 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: 'nsg-snet-backend-${environment}'
location: location
properties: {
securityRules: [
{
name: 'Allow-AppGateway-Inbound'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: '10.0.1.0/24' // App Gateway subnet
sourcePortRange: '*'
destinationAddressPrefix: '10.0.2.0/24'
destinationPortRange: '443'
description: 'Allow HTTPS from App Gateway subnet only'
}
}
{
name: 'Deny-All-Inbound'
properties: {
priority: 4000
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
description: 'Deny all other inbound traffic'
}
}
]
}
}
NSG Rules When Using Private Endpoints
Private endpoints have specific NSG requirements:
| Rule | Source | Destination | Port | Notes |
|---|---|---|---|---|
| Allow App Gateway → PE | 10.0.1.0/24 |
10.0.2.0/24 |
443 |
App Gateway to backend private endpoints |
| Allow Health Probes | AzureLoadBalancer |
10.0.2.0/24 |
* |
Azure internal health checking |
| Deny All Inbound | * |
* |
* |
Explicit deny at a lower priority |
Note: NSG support on private endpoints must be enabled on the subnet. Set
privateEndpointNetworkPolicies: 'Enabled'on the subnet properties.
{
name: 'snet-backend'
properties: {
addressPrefix: '10.0.2.0/24'
privateEndpointNetworkPolicies: 'Enabled' // Required to apply NSG to PEs
networkSecurityGroup: {
id: nsgBackend.id
}
}
}
Step 8 — SSL/TLS Certificate Configuration
Key Vault Integration (Zero Trust Approach)
Never store certificates directly on the Application Gateway. Use Key Vault with managed identity:
// User-assigned managed identity for App Gateway
resource appGwIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'id-appgw-${environment}'
location: location
}
// Key Vault access policy for the managed identity
resource kvAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2023-07-01' = {
parent: keyVault
name: 'add'
properties: {
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: appGwIdentity.properties.principalId
permissions: {
secrets: [ 'get' ]
certificates: [ 'get' ]
}
}
]
}
}
Certificate Rotation
Application Gateway automatically fetches renewed certificates from Key Vault every 4 hours. Ensure:
- Certificate is renewed in Key Vault before expiry
- The new certificate retains the same secret name
- The managed identity still has
GETpermissions
Step 9 — WAF Rule Tuning for the New Backend
When adding a new backend, its traffic patterns may trigger WAF false positives. Follow this process:
1. Deploy in Detection Mode First
If adding a rule to an existing Production Application Gateway that already runs WAF in Prevention mode, use a per-listener WAF policy override to run the new rule in Detection mode:
// Per-listener WAF policy (Detection mode for new backend)
resource wafPolicyOrders 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2023-09-01' = {
name: 'waf-policy-orders-${environment}'
location: location
properties: {
policySettings: {
state: 'Enabled'
mode: 'Detection' // Detection for tuning
requestBodyCheck: true
maxRequestBodySizeInKb: 128
}
managedRules: {
managedRuleSets: [
{
ruleSetType: 'OWASP'
ruleSetVersion: '3.2'
}
]
}
}
}
2. Monitor for 1–2 Weeks
// KQL: Find false positives for the new backend
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where hostname_s == "orders.contoso.com"
| where action_s == "Detected"
| summarize Count = count() by ruleId_s, ruleGroup_s, details_message_s
| order by Count desc
3. Add Exclusions for Confirmed False Positives
4. Switch to Prevention Mode
Step 10 — End-to-End Verification Checklist
After deploying the new rule, verify every layer systematically:
DNS Resolution
# From a VM in the App Gateway VNet
nslookup app-orders-prod.azurewebsites.net
# Must return private IP (10.x.x.x), NOT public IP
# If using legacy AD DNS, test from a domain-joined machine
nslookup app-orders-prod.azurewebsites.net dc01.contoso.com
# Verify the conditional forwarder chain works
Application Gateway Backend Health
# Check backend health via Azure CLI
az network application-gateway show-backend-health \
--resource-group rg-networking-prod \
--name agw-integration-prod \
--query 'backendAddressPools[?backendAddressPool.id contains `pool-orders`]' \
--output table
Expected: Healthy. If Unhealthy, check:
| Symptom | Likely Cause | Fix |
|---|---|---|
| Health probe failing | NSG blocking App Gateway → Backend | Add NSG rule allowing 10.0.1.0/24 → 10.0.2.0/24:443 |
| Health probe failing | DNS not resolving to private IP | Check Private DNS zone link to VNet |
| Health probe failing | Backend returning 4xx/5xx | Check probe path and host header settings |
Backend shows Unknown |
GatewayManager ports blocked | Allow inbound 65200-65535 from GatewayManager |
SSL Certificate Validation
# Test SSL from App Gateway's perspective
az network application-gateway ssl-cert list \
--resource-group rg-networking-prod \
--gateway-name agw-integration-prod \
--output table
# Verify certificate expiry
az keyvault certificate show \
--vault-name kv-certs-prod \
--name wildcard-cert \
--query 'attributes.expires' \
--output tsv
NSG Flow Logs
Enable NSG flow logs to verify traffic is flowing correctly:
// KQL: Check if traffic from App Gateway is reaching the backend
AzureNetworkAnalytics_CL
| where SubType_s == "FlowLog"
| where SrcIP_s startswith "10.0.1." // App Gateway subnet
| where DestIP_s startswith "10.0.2." // Backend subnet
| where DestPort_d == 443
| where FlowStatus_s == "A" // Allowed
| summarize Count = count() by bin(TimeGenerated, 5m)
| render timechart
WAF Logs
// KQL: Check for blocked requests to the new backend
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where hostname_s == "orders.contoso.com"
| where action_s == "Blocked"
| project TimeGenerated, ruleId_s, ruleGroup_s, details_message_s, requestUri_s
| order by TimeGenerated desc
| take 50
End-to-End Connectivity Test
# From outside the network — verify the full path
curl -v https://orders.contoso.com/health
# Expected: HTTP 200
# Verify headers include your rewrite rules (HSTS, X-Content-Type-Options, etc.)
Troubleshooting Guide
502 Bad Gateway
| Check | Command / Action |
|---|---|
| Backend health | Portal → App Gateway → Backend Health |
| DNS resolution | nslookup <backend-fqdn> from App GW VNet — must return private IP |
| NSG rules | Verify App GW subnet can reach backend subnet on port 443 |
| Host header | Ensure pickHostNameFromBackendAddress or custom hostName is correct |
| Backend certificate | If using end-to-end SSL, ensure trusted root cert is configured |
| Probe path | Verify the health probe path returns 200 on the backend |
403 Forbidden
| Check | Action |
|---|---|
| WAF blocking | Check Firewall logs for blocked rule IDs |
| Backend access restriction | If App Service, check IP restrictions allow App GW subnet |
| Custom WAF rules | Check custom rule priorities — a broad block rule may match first |
Backend Shows Unhealthy
| Check | Action |
|---|---|
| Probe timeout | Increase from 10s to 30s if backend is slow to start |
| Probe interval | Reduce interval for faster detection |
| Status code match | Ensure match.statusCodes includes codes returned by your health endpoint |
| Private endpoint approved | Check private endpoint connection status is Approved |
DNS Resolution Returns Public IP
| Check | Action |
|---|---|
| Private DNS zone exists | Verify privatelink.azurewebsites.net zone exists |
| VNet link active | Verify zone is linked to the App Gateway VNet |
| Private endpoint DNS group | Verify privateDnsZoneGroups is configured on the PE |
| AD conditional forwarder | Verify forwarder for privatelink.* points to Azure DNS Private Resolver |
| VNet DNS settings | If custom DNS servers, verify forwarder chain resolves correctly |
Security Hardening Checklist
| Control | Status | Notes |
|---|---|---|
| WAF v2 SKU with Prevention mode | Required | Never use Standard SKU in Zero Trust |
| OWASP 3.2 rule set enabled | Required | Plus Bot Manager 1.1 |
| End-to-end SSL (TLS 1.2+) | Required | No unencrypted backend connections |
SSL Policy AppGwSslPolicy20220101 |
Required | Enforces TLS 1.2 with strong ciphers |
| Certificates in Key Vault | Required | Never store PFX on the gateway directly |
| Managed identity for Key Vault access | Required | No shared secrets or keys |
| NSG on every subnet | Required | Explicit deny-all with specific allows |
| GatewayManager ports open (65200-65535) | Required | App Gateway will fail without this |
| DDoS Protection Standard | Recommended | On the VNet |
| Private endpoints for all backends | Required | No public internet routing for backends |
| Private DNS zones linked | Required | Backends must resolve to private IPs |
| Diagnostic logs to Log Analytics | Required | Access, Performance, and Firewall logs |
| NSG Flow Logs enabled | Recommended | For traffic auditing and troubleshooting |
| HTTP-to-HTTPS redirect | Required | Never serve over plain HTTP |
| Security headers via rewrite rules | Recommended | HSTS, X-Content-Type-Options, X-Frame-Options |
| Custom WAF rules for rate limiting | Recommended | Protect against brute force and DDoS |