Azure Policy Defense-in-Depth: Enforcing Firewall Routes That Can't Be Bypassed
Most organizations enforce firewall routing with a single Azure Policy. That's not enough. Here's a defense-in-depth strategy using four policies: one that auto-fixes, one that blocks bad configs, one that prevents deletion, and one that ensures every subnet has a route table. Together, they're virtually impossible to bypass.
Table of Contents
The Problem with Single-Policy Enforcement
In any secure Azure environment, especially government and financial services, you need all network traffic to flow through a central firewall or network virtual appliance. The standard approach is a default route (0.0.0.0/0) in every Route Table pointing to the firewall's private IP.
Most teams enforce this with a single Deny policy. But this creates three gaps:
- No auto-remediation. Teams must manually add the route every time, creating friction and delaying deployments.
- No deletion protection. A standard Deny policy blocks creation and modification but NOT deletion. Anyone with the right RBAC permissions can simply delete the firewall route.
- No subnet enforcement. A subnet without any Route Table uses Azure's default system routes, completely bypassing the firewall.
The Defense-in-Depth Strategy
┌──────────────────────────────────────────────────────────────┐
│ Firewall Route Enforcement Initiative │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ THE FIXER │ │ THE GUARD │ │ THE LOCK │ │
│ │ (Modify) │ │ (Deny) │ │ (DenyAction) │ │
│ │ │ │ │ │ │ │
│ │ Auto-adds │ │ Blocks wrong │ │ Prevents │ │
│ │ missing route │ │ route configs │ │ route deletion│ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
│ ┌────────────────┐ │
│ │ THE ENFORCER │ Each policy covers a specific attack │
│ │ (Deny) │ vector. Together, they ensure the route │
│ │ │ always exists, always points to the │
│ │ No subnet │ right place, can't be deleted, and │
│ │ without RT │ every subnet is covered. │
│ └────────────────┘ │
└──────────────────────────────────────────────────────────────┘Policy 1: The Fixer (Auto-Add Firewall Route)
Effect: Modify
When someone creates a new Route Table without the required default route, instead of blocking the deployment, this policy automatically injects the correct route into the request before the resource is created. No manual steps, no friction.
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/routeTables" },
{
"count": {
"field": "Microsoft.Network/routeTables/routes[*]",
"where": {
"allOf": [
{ "field": "...routes[*].addressPrefix", "equals": "0.0.0.0/0" },
{ "field": "...routes[*].nextHopType", "equals": "VirtualAppliance" },
{ "field": "...routes[*].nextHopIpAddress",
"equals": "[parameters('firewallIP')]" }
]
}
},
"equals": 0
}
]
},
"then": {
"effect": "modify",
"details": {
"operations": [{
"operation": "add",
"field": "Microsoft.Network/routeTables/routes[*]",
"value": {
"name": "firewall-default-route",
"properties": {
"addressPrefix": "0.0.0.0/0",
"nextHopType": "VirtualAppliance",
"nextHopIpAddress": "[parameters('firewallIP')]"
}
}
}]
}
}
}Modify runs inline during the ARM deployment, so the route is added before the resource is created. DeployIfNotExists runs after the fact and requires a remediation task, leaving a window where the Route Table exists without the firewall route.
Policy 2: The Guard (Deny Invalid Firewall Route)
Effect: Deny
The security gatekeeper. It validates both Route Table resources and individual Route child resources. If anyone tries to point 0.0.0.0/0 to any IP other than the approved firewall, the request is blocked.
Two evaluation branches prevent bypass attempts:
- Route Table level: Checks if the full Route Table contains the correct mapping
- Individual Route level: Catches cases where someone updates a single route within an existing Route Table
Without the individual Route check, a user could create a valid Route Table (passes parent check), then update the specific route to point to a different IP. The second branch catches this attack vector.
Policy 3: The Lock (Deny Deletion of Firewall Route)
Effect: DenyAction
The policy most organizations miss. Standard Deny effects only evaluate resource creation and updates. They do not prevent deletion. DenyAction specifically targets the delete action on a named route resource.
{
"if": {
"allOf": [
{ "field": "type",
"equals": "Microsoft.Network/routeTables/routes" },
{ "field": "name",
"equals": "firewall-default-route" }
]
},
"then": {
"effect": "DenyAction",
"details": {
"actionNames": [ "delete" ]
}
}
}The simplest policy in the set, but arguably the most important. Without it, any user with Network Contributor can delete the route and bypass the firewall entirely.
Policy 4: The Enforcer (Subnets Must Have Route Table)
Effect: Deny
The final gap: a subnet without any Route Table uses Azure's default system routes, sending traffic directly to the internet and bypassing the firewall completely.
{
"if": {
"allOf": [
{ "field": "type",
"equals": "Microsoft.Network/virtualNetworks/subnets" },
{ "field": "...subnets/routeTable.id",
"exists": false }
]
},
"then": { "effect": "Deny" }
}Bundling as an Initiative
All four policies should be deployed as a Policy Initiative (Policy Set Definition). Benefits:
- Single assignment instead of four separate ones
- Centralized parameters (firewall IP defined once)
- Per-policy effect control (disable auto-add without affecting deny policies)
- Unified compliance reporting
The initiative parameterizes the firewall IP and each policy's effect, so you can switch Deny to Audit for initial rollout without touching the definitions.
Testing Scenarios
| Scenario | Expected Result | Policy |
|---|---|---|
| Create Route Table without firewall route | Success. Route auto-added. | The Fixer |
| Change default route to wrong IP | Blocked. | The Guard |
| Delete the firewall route | Blocked. | The Lock |
| Create subnet without Route Table | Blocked. | The Enforcer |
| Create Route Table with correct route | Success. | None (compliant) |
Operations & Troubleshooting
Legitimate Changes
If you need to change the firewall IP (e.g., during a migration), either update the initiative parameter and trigger a remediation scan, or temporarily exempt the assignment at the target scope.
Initial Rollout
Start with Deny policies in Audit mode. Review compliance, confirm all Route Tables are correct, then switch to Deny.
Get the Code
All four policy definitions, the initiative JSON, and deployment instructions.
View on GitHub →