Brewing a Secure Hub-and-Spoke in Azure - Azure Application Gateway

Application Gateway + WAF: No, it’s not “just another firewall”

You wouldn’t ask your local barista to start pouring pints behind the pub bar. The same goes for security controls in Azure. Azure Firewall and Application Gateway WAF are both vital, but they serve different purposes. Get them confused and you’ll either overspend or under‑protect.

What is Application Gateway WAF?

Application Gateway (AppGW) is a Layer 7 load balancer with optional WAF (Web Application Firewall) capabilities. Think of it as traffic control at the HTTP/S level. The WAF inspects requests bound for web apps, protecting against OWASP Top 10 threats like SQLi and XSS.

Key features include:

  • Reverse proxy with SSL offload, end-to-end TLS, and SNI support
  • WAF in Detection or Prevention mode (based on OWASP rulesets)
  • HTTP header rewrites, redirects, URL-based routing
  • Autoscaling and zone redundancy for resilience

It doesn’t replace Azure Firewall. Instead, it complements it.

How They Differ: AppGW WAF vs Azure Firewall

  • Azure Firewall:

    • L3–L7 controls
    • East-west + north-south traffic
    • TLS inspection, IDPS, Threat Intel
    • Network-centric enforcement
  • App Gateway WAF:

    • L7, application-specific inspection
    • Inbound HTTP/S (north-south only)
    • OWASP rule protection for web apps
    • Application-centric enforcement

In short: Firewall protects networks, WAF protects apps.

Deployment Models

Two options exist, each used in production depending on needs:

  1. Side-by-Side

    • Azure Firewall secures all traffic centrally.
    • Application Gateway WAF directly fronts the web apps.
    • Internet → App Gateway WAF → Web App (spoke)
    • Outbound/other spoke traffic → Firewall
  2. Chained

    • Azure Firewall sits in front of App Gateway.
    • Firewall does ingress filtering, threat intel, TLS inspection.
    • Clean traffic is then forwarded to App GW for WAF and app‑layer routing.
    • Internet → Firewall → App Gateway WAF → Web App
flowchart TD Internet --> FW[Azure Firewall Premium] FW --> AGW[Application Gateway + WAF] AGW --> WebApp[Web App in Spoke VNet] FW --> SpokeDB[Other Spokes / Data Tiers]

Real-World Example

Inbound web traffic:

  • Users access public web apps hosted in Spoke VNets.
  • Azure Firewall Premium inspects TLS traffic, applies IDPS and Threat Intel checks.
  • Traffic is then allowed to Application Gateway WAF, which applies OWASP rules to the HTTP/S requests.
  • Clean, authorised traffic is then routed to backend VMs or App Service instances.

This setup marries the strength of both services, reducing blind spots.

Implementation Examples

Azure Portal Quick Steps

  1. Deploy Azure Firewall Premium in Hub VNet.
  2. Deploy Application Gateway (with WAF v2) into its own subnet, either in Hub or a Spoke.
  3. Configure Firewall UDRs to steer inbound HTTP/S traffic via Firewall → AppGW → WebApp.
  4. Enable WAF in Prevention mode with the latest OWASP ruleset.
  5. Set App Gateway Listener with custom domain + SSL cert.

Bicep Example — App Gateway WAF v2 Skeleton

To continue on our previous post where we split things into modules we have created a few new resources in order to enable this feature set.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/bicep
  ├─ main.bicep              # Entry point
  ├─ modules/
  |   ├─ azappgateway.bicep  # Azure Application Gateway
  |   ├─ azfw.bicep          # Azure Firewall
  |   ├─ azfwpolicy.bicep    # Azure Firewall Policy
  |   ├─ azfwpolicypremium.bicep    # Azure Firewall Premium Policy
  |   ├─ keyvault.bicep      # Azure KeyVault + Managed Identity (Required for TLS Inspection)
  │   ├─ hub.bicep           # Hub VNet + Firewall Subnet
  │   ├─ spoke.bicep         # Spoke VNet + NSG + UDR
  │   └─ peering.bicep       # Hub-Spoke peering

🔹 main.bicep — Root deployment updated to include additional resource changes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Key Vault needs another new Certificate for the Application Gateway
module keyVault './modules/keyvault.bicep' = {
  name: 'keyVaultDeployment'
  params: {
    location: location
    certName: 'AzFwTlsCert'
    certSubject: 'CN=AzFwTlsCert.brewedinthecloud.com'
    keyVaultName: 'kv-azfw-hub'
    appgwcertName: 'AppGwTlsCert' //I'm using self signed in the example
  }  
}

// Deploy Azure Application Gateway
module appGateway './modules/azappgw.bicep' = {
  name: 'appGatewayDeployment'
  params: {
    location: location
    subnetNameid: hub.outputs.appgwSubnetId
    appGwName: 'appgw-hub'
    appGwPublicIPName: 'appgw-hub-publicIP'
    keyVaultId: keyVault.outputs.keyVaultId
    appGwCertName: keyVault.outputs.appgwcertName
  }
}

🔹 modules/hub.bicep — Our secure hub virtual network needs an additional subnet for the Application Gateway

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
resource hubVnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
  name: hubVnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [hubAddressPrefix]
    }
    subnets: [
      {
        name: 'AzureFirewallSubnet'
        properties: { addressPrefix: '10.0.1.0/24' }
      }
      {
        name: 'GatewaySubnet'
        properties: { addressPrefix: '10.0.255.0/27' }
      }
      {
        name: 'SharedServices'
        properties: { addressPrefix: '10.0.2.0/24' }
      }
      {
        name: 'ApplicationGateway'
        properties: { addressPrefix: '10.0.3.0/24' }
      }
    ]
  }
}

output appgwSubnetId string = hubVnet.properties.subnets[3].id

🔹 modules/azappgateway.bicep — Deploy Azure Application Gateway with WAFv2

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
param location string
param subnetNameid string
param appGwName string
param appGwPublicIPName string
param appGwCertName string
param keyVaultId string

resource appGwPublicIP 'Microsoft.Network/publicIPAddresses@2023-05-01' = {
  name: appGwPublicIPName
  location: location
  sku: {  
    name: 'Standard'
    tier: 'Regional'
  }
}

resource appgwwafpolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2024-07-01' = {
  name: '${appGwName}-wafpolicy'
  location: location
  properties: {
    policySettings: {
      enabledState: 'Enabled'
      mode: 'Prevention'
      requestBodyCheck: true
      maxRequestBodySizeInKb: 128
      fileUploadLimitInMb: 100
      exclusions: []
    }
    customRules: []
    managedRules: {
      managedRuleSets: [
        {
          ruleSetType: 'OWASP'
          ruleSetVersion: '3.2'
          ruleGroupOverrides: []
        }
      ]
    }
  }
}

resource appGw 'Microsoft.Network/applicationGateways@2023-05-01' = {
  name: appGwName
  location: location
  sku: {
    name: 'WAF_v2'
    tier: 'WAF_v2'
    capacity: 2
  }
  properties: {
    autoscaleConfiguration: {
      minCapacity: 2
    }
    gatewayIPConfigurations: [
      {
        name: 'appGwIpConfig'
        properties: {
          subnet: {
            id: subnetNameid
          }
        }
      }
    ]
    frontendIPConfigurations: [
      {
        name: 'appGwFrontendIP-port443'
        properties: {
          publicIPAddress: {
            id: appGwPublicIP.id
          }
        }
      }
    ]
    frontendPorts: [
      {
        name: 'appGwFrontendPort-443'
        properties: {
          port: 443
        }
      }
    ]
    backendAddressPools: [
      {
        name: 'appGwBackendPool'
        properties: {
          backendAddresses: [
          ]
        }
      }
    ]
    backendHttpSettingsCollection: [
      {
        name: 'appGwBackendHttpSettings'
        properties: {
          port: 80
          protocol: 'Http'
          cookieBasedAffinity: 'Disabled'
          pickHostNameFromBackendAddress: false
          requestTimeout: 20
        }
      }
    ]
    httpListeners: [
      {
        name: 'appGwHttpsListener'
        properties: {
          frontendIPConfiguration: {
            id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', appGwName, 'appGwFrontendIP')
          }
          frontendPort: {
            id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGwName, 'appGwFrontendPort')
          }
          protocol: 'Https'
          sslCertificate: {
            id: '${keyVaultId}/certificates/${appGwCertName}'
          }
          requireServerNameIndication: false
        }
      }
    ]
    firewallPolicy: {
      id: appgwwafpolicy.id
    }
    enableHttp2: true
  }
}

Gotchas & Edge Cases

  • App Gateway Placement — Deploy in its own subnet; don’t cram it into shared services.
  • Chaining Adds Latency — More security checks = more hops. Test carefully for user‑facing apps.
  • Certificates — Both Firewall TLS inspection and App Gateway TLS termination need managed cert lifecycle.
  • Costs — Running WAF + Firewall Premium together isn’t cheap. Justify with compliance or risk posture.

Best Practices

  • Place App Gateway close to app workloads (same region, minimal latency).
  • Use WAF_v2 or higher for autoscaling and zone redundancy.
  • Only chain Firewall + App Gateway when you need both network + app layer security on inbound flows.
  • Feed WAF logs into Log Analytics to correlate alerts with Firewall.
🍺
Brewed Insight: App Gateway WAF is not a general-purpose firewall. It’s a purpose-built HTTP/S filter for protecting web apps, and it plays best when paired with Azure Firewall. Call it the hops to your malt — neither alone makes a great beer, but together they give it balance.

Learn More