Azure Firewall
Azure Firewall is a managed, stateful network firewall that provides centralized outbound and inbound traffic control for a hub-spoke network. In an Epic IRE it sits in the hub VNet and acts as the single egress chokepoint: spoke subnets route their outbound traffic through it, and it filters that traffic against a policy before it reaches the internet or other networks.
The firewall stack is three resources that work together:
- Firewall (
firewall) — the firewall instance, deployed in the hub'sAzureFirewallSubnet. - Firewall Policy (
firewall_policy) — the policy attached to the firewall. It holds the threat-intelligence, DNS, and intrusion-detection settings, and owns the rules. - Rule Collection Groups (nested under the policy as
rule_collection_groups) — the network, application, and NAT rules that decide what traffic is allowed.
Available features:
- Rule filtering — network rules (L3/L4), application rules (FQDN/HTTP-S), and NAT rules (inbound DNAT).
- Threat intelligence — alert or deny traffic to/from known-malicious IPs and domains.
- DNS proxy — the firewall acts as DNS server for spokes, enabling FQDN rules to resolve consistently.
- Intrusion Detection and Prevention (IDPS) — signature-based detection (requires
PremiumSKU). - Forced tunneling — force spoke subnets' outbound through the firewall via a route table.
- Logging — ship traffic/threat logs to a Log Analytics Workspace.
- Availability zones — deploy zone-redundant for resilience.
Basic Configuration
The minimum is a policy and a firewall that references it. The firewall needs a dedicated AzureFirewallSubnet (created via the networks variable) and a Standard/Static public IP (created via the public_ips variable).
This creates a firewall and policy with the following defaults:
- Firewall SKU:
AZFW_VNet/Standard - Policy SKU:
Standard - Threat intelligence:
Alert(alerts on malicious traffic but does not block) - DNS proxy: disabled
- Availability zones: none (not zone-redundant)
- Rules: none — a firewall with no rules denies all traffic routed through it
- Logging: none
- Location: uses the global location set in tfvars
Note: A firewall with no rule collection groups blocks everything routed through it. Add rules (below) for the traffic the IRE needs before forcing any subnet through the firewall.
Rules
Rules live in rule_collection_groups, a map nested inside the policy. Each group has a priority (100-65000) and holds any mix of three collection types. Lower priority numbers evaluate first.
network_rule_collection— L3/L4 rules by IP/port/protocol. Use for non-HTTP traffic (DNS, NTP, database ports, etc.).application_rule_collection— L7 rules by FQDN. Use for HTTP/HTTPS egress to named hosts (e.g.*.microsoft.com).nat_rule_collection— inbound DNAT rules that translate a public IP/port to a private destination.
Note: The rule collection group inherits its owning policy from the parent entry — you do not set firewall_policy on it.
Tip: In an application_rule, protocols is a list of { type, port } objects (type is Http/Https). In network_rule and nat_rule, protocols is a plain list of strings (TCP, UDP, ICMP, Any).
NAT (DNAT) rules
A nat_rule_collection translates inbound traffic arriving at one of the firewall's public IPs to a private destination. The action is always Dnat, and each rule's destination must be one of the firewall's own public IPs.
Reference that public IP by key with destination_public_ip (a key from the public_ips variable) rather than hard-coding the address — the module resolves it to the allocated IP internally, so the rule self-heals if the IP ever changes. Use the literal destination_address only when the IP is managed outside this stack.
Note: destination_public_ip and destination_address are alternatives — if both are set, destination_public_ip wins. The referenced public IP must be one attached to this firewall; Azure rejects a DNAT rule whose destination is not a firewall public IP. A NAT rule has a single destination (destination_address is a string, not a list), so to DNAT multiple public IPs, define one rule per destination.
Threat Intelligence
Threat-intelligence filtering alerts on or blocks traffic to/from IPs and domains in the Microsoft threat feed. Set threat_intelligence_mode on the policy:
Off— disabled.Alert— log high-confidence hits but allow the traffic (default).Deny— block and alert (this is the portal's "Alert and deny").
DNS Proxy
With DNS proxy enabled, spokes point their DNS at the firewall's private IP, and the firewall forwards queries. This keeps FQDN-based application rules consistent (the firewall resolves the same answer it filters on) and is required for FQDN filtering in network rules.
Availability Zones
For production resilience, deploy the firewall zone-redundant across all three zones. The firewall's public IP must also be zonal.
Note: zones is immutable on both the firewall and the public IP — changing it on an existing resource forces a destroy and recreate. Set it at initial deployment.
Forced Tunneling / Outbound Routing
A subnet does not send its outbound traffic through the firewall just because the firewall exists. You force it by attaching a route table (UDR) to the subnet with a 0.0.0.0/0 route whose next hop is the firewall's private IP. This is the pattern for routing all internet-bound traffic through the firewall — and an explicit outbound method like this is required for new virtual networks created with API versions after March 31, 2026, when Azure defaults new VNets to private subnets with no default outbound internet access.
Reference the firewall by key with next_hop_firewall rather than hard-coding its private IP. The route module resolves the firewall's private IP internally, so the route self-heals if the firewall is ever recreated (for example when zones are added).
Then attach the route table to the subnet whose outbound you want to force, via the route_table key on that subnet in the networks variable:
Note: Once traffic is forced through the firewall it is dropped unless a rule allows it. Make sure the policy permits the subnet's required outbound (an application rule for HTTPS FQDNs, a network rule for DNS, etc.) before relying on the route — otherwise outbound will black-hole.
Logging
There are two independent logging mechanisms, and they cover different things:
- Policy Insights (
firewall_policy.insights) — drives the Firewall Policy Analytics workbook (top flows, rule-hit insights). It does not ship the raw traffic logs. - Diagnostic Settings (
firewall.diagnostic_settings) — ships the actual firewall resource logs (rule hits, threat intel, IDPS, DNS) to a Log Analytics Workspace, Storage Account, or Event Hub. This is the one to use for traffic logging and SIEM ingestion.
Reference the destination Log Analytics Workspace by key (from the log_analytics_workspace variable). Use log_analytics_destination_type = "Dedicated" for the recommended resource-specific AZFW* tables.
Note: Dedicated writes to the structured AZFW* tables (recommended — cheaper, easier to query). Omitting it falls back to the legacy single AzureDiagnostics table, which uses different category names (AzureFirewallApplicationRule, AzureFirewallNetworkRule, AzureFirewallDnsProxy).
Note: The diagnostic category names differ from the Log Analytics table names in the Microsoft docs (e.g. the category is AZFWFqdnResolveFailure, not the table name AZFWInternalFqdnResolutionFailure). A wrong name fails at apply with 400 BadRequest: Category '<x>' is not supported. Confirm the supported list against the live resource:
az monitor diagnostic-settings categories list \
--resource <firewall-name> --resource-group <rg> \
--resource-type "Microsoft.Network/azureFirewalls" \
--query "value[?categoryType=='Logs'].name" -o tsv
Note: AZFWDnsQuery only produces data when DNS proxy is enabled. The flow-trace categories (AZFWFatFlow, AZFWFlowTrace, AZFWDnsAdditional) require extra firewall runtime toggles and add cost, so they are omitted from the default set.
Policy Insights
firewall_policy.insights enables Firewall Policy Analytics. Set enabled, a default_log_analytics_workspace (a key from the log_analytics_workspace variable), and an optional retention_in_days.
For a firewall fleet that spans regions, add the optional log_analytics_workspace list to route each firewall's analytics to a region-local workspace. Each entry maps a firewall_location (Azure region) to a workspace key; firewalls in a region with no matching entry fall back to default_log_analytics_workspace.
Note: Both workspace and default_log_analytics_workspace are keys from the log_analytics_workspace variable — the module resolves them to workspace IDs. Omit the log_analytics_workspace list entirely for a single-region deployment; default_log_analytics_workspace is sufficient on its own.
Configuration Parameters
Firewall Policy
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
resource_group |
string | Yes | - | Resource group key the policy is created in |
name |
string | No | Auto-generated | Custom policy name; otherwise uses the naming convention |
location |
string | No | Global location | Azure region |
sku |
string | No | Standard |
Standard, Premium, or Basic. Premium is required for IDPS |
threat_intelligence_mode |
string | No | Alert |
Off, Alert, or Deny |
threat_intelligence_allowlist |
object | No | null |
fqdns / ip_addresses skipped for threat detection |
dns |
object | No | null |
proxy_enabled (bool) and optional servers (list) |
intrusion_detection |
object | No | null |
IDPS mode, private_ranges, overrides, bypass. Requires Premium |
insights |
object | No | null |
Policy Analytics: enabled, default_log_analytics_workspace (key), retention_in_days, and an optional per-region log_analytics_workspace list (each entry: workspace key + firewall_location) |
rule_collection_groups |
map(object) | No | {} |
Rule collection groups owned by this policy (see below) |
existing |
bool | No | false |
Reference a pre-existing policy instead of creating it |
tags |
map(string) | No | {} |
Tags merged with default tags |
Firewall
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
resource_group |
string | Yes | - | Resource group key the firewall is created in |
name |
string | No | Auto-generated | Custom firewall name; otherwise uses the naming convention |
location |
string | No | Global location | Azure region |
sku_name |
string | No | AZFW_VNet |
AZFW_VNet or AZFW_Hub |
sku_tier |
string | No | Standard |
Standard, Premium, or Basic |
firewall_policy |
string | No | null |
Key of the policy to attach |
dns_servers |
list(string) | No | null |
DNS servers the firewall directs DNS traffic to |
dns_proxy_enabled |
bool | No | null |
Enable DNS proxy (when not governed by a policy) |
private_ip_ranges |
list(string) | No | null |
SNAT private CIDR ranges, or IANAPrivateRanges |
threat_intel_mode |
string | No | Alert |
Off, Alert, or Deny (when not governed by a policy) |
zones |
list(string) | No | null |
Availability zones, e.g. ["1","2","3"] |
ip_configuration |
list(object) | Yes | - | name, subnet (key), public_ip_address (key). Exactly one entry may set subnet |
management_ip_configuration |
object | No | null |
Separate management IP config for forced tunnelling |
diagnostic_settings |
map(object) | No | {} |
Log destinations (see Logging) |
existing |
bool | No | false |
Reference a pre-existing firewall instead of creating it |
tags |
map(string) | No | {} |
Tags merged with default tags |
Note: When a firewall_policy is attached, threat intel and DNS are governed by the policy's settings, not the firewall's threat_intel_mode / dns_proxy_enabled fields.
Note: subnet and public_ip_address are keys from the networks and public_ips variables, not IDs. The firewall's subnet must be named AzureFirewallSubnet and be at least a /26.
Rule Collection Group
Each entry of rule_collection_groups. The owning policy is inherited from the parent, so there is no firewall_policy field.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
priority |
number | Yes | - | Group priority, 100-65000 (lower evaluates first) |
name |
string | No | Auto-generated | Custom group name |
network_rule_collection |
list(object) | No | [] |
L3/L4 rules. Each: name, action, priority, rule |
application_rule_collection |
list(object) | No | [] |
L7/FQDN rules. Each: name, action, priority, rule |
nat_rule_collection |
list(object) | No | [] |
Inbound DNAT rules. Each: name, action, priority, rule |
Diagnostic Settings
Each entry of diagnostic_settings.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
workspace |
string | No | null |
Log Analytics Workspace key destination |
log_analytics_destination_type |
string | No | null |
Dedicated (resource-specific tables) or AzureDiagnostics (legacy) |
enabled_logs |
object | No | {} |
categories (list) or category_groups (e.g. allLogs) — not both |
enabled_metrics |
list(string) | No | null |
Metric categories (e.g. AllMetrics) |
storage_account |
string | No | null |
Storage Account key destination |
eventhub_authorization_rule_id |
string | No | null |
Event Hub authorization rule ID |
eventhub_name |
string | No | null |
Event Hub name |
name |
string | No | firewall-<key>-<entry> |
Custom diagnostic setting name |
Note: At least one destination must be set, and enabled_logs cannot define both categories and category_groups (enforced by a module precondition).
Naming Convention
Each resource name follows the {prefix}{key}{suffix} pattern unless an explicit name is set on the entry. For a firewall keyed hub, with prefix ire- and suffix -afw, the name resolves to ire-hub-afw.
The rule collection group name comes from its own (child) key; the composite "<policy key>.<group key>" key is used only internally for iteration.
The following prefix/suffix map entries are required:
name_prefixes = {
firewall = "ire-"
firewall_policy = "ire-"
firewall_policy_rule_collection_group = "ire-"
}
name_suffixes = {
firewall = "-afw"
firewall_policy = "-afwp"
firewall_policy_rule_collection_group = "-rcg"
}
Best Practices
- Use a policy. Attach a
firewall_policyand put all rules in itsrule_collection_groupsrather than on the firewall directly — policies support reuse, inheritance, and Policy Analytics. - Deny by default, allow explicitly. Start with no rules (deny-all) and add only the egress the IRE needs. Force subnets through the firewall only after the allow rules exist.
- Enable threat intelligence in
Denyfor production to block known-malicious traffic, not just alert. - Deploy zone-redundant (
zones = ["1","2","3"]) for production, and set zones on the public IP to match. Decide this at initial deployment — it is immutable. - Enable logging to a Log Analytics Workspace in
Dedicatedmode for traffic visibility and forensics. - Enable DNS proxy when using FQDN-based application rules so resolution stays consistent.
- Reference the firewall by key in routes (
next_hop_firewall) so forced-tunnel routes survive a firewall recreate.
Epic IRE Example
A consolidated, opinionated example for an Epic IRE — zone-redundant, threat intelligence in Deny, DNS proxy on, logging to Log Analytics, and a baseline Epic rule set — is coming soon.