Networking
Azure Firewall: qualify a broad rule before tightening production access
A production runbook for tightening an Azure Firewall rule without breaking useful traffic, with inventory, KQL evidence, dependencies, controlled validation and rollback.
A broad Azure Firewall rule tends to become invisible. It was added to unblock an incident, absorb a migration, let a partner flow through, or buy time during a deployment. A few weeks later, nobody knows whether it still protects a real dependency, hides a DNS problem, or simply allows too many destinations.
The use case is a hub-and-spoke Azure environment where outbound traffic crosses Azure Firewall. An application or network rule allows too much: a wildcard FQDN, a generous IP range, a port shared by several workloads, or a high-priority collection inherited from an emergency. The goal is not to delete it bravely. This runbook helps decide whether the rule can be tightened, expired, split or temporarily kept with usable evidence.
Treat the rule as a production risk
Before changing the policy, describe the rule as a production contract. A Firewall rule is not just configuration. It encodes assumptions about workloads, dependencies, protocols, ports, network identities and the acceptable risk of blocking traffic.
Rule to qualify
Policy: azfw-policy-prod
Rule collection group: rcg-egress-prod
Rule collection: app-egress-shared
Rule: allow-partner-or-temporary-wildcard
Type: application or network
Expected sources: snet-app-prod, snet-jobs-prod
Expected destinations: api.partner.example, auth.partner.example
Ports: 443
Rule origin: incident, migration, security exception or deployment
Creation ticket or date
Business owner and platform owner
Questions before tightening
Which real flows still use the rule?
Does traffic come from the expected sources?
Do observed destinations match the documented need?
Is a higher-priority rule hiding the result?
Which test proves tightening will not break production?
Which rollback restores the previous state without widening further? If the team cannot answer these questions, the right first action is evidence collection, not rule removal.
Read hits before intentions
Rule documentation explains why it exists. Logs show what it actually does. Start by isolating events for the collection, rule and expected sources.
let StartTime = ago(14d);
let RuleName = "allow-partner-or-temporary-wildcard";
AZFWApplicationRule
| where TimeGenerated > StartTime
| where Rule == RuleName
| summarize
hits = count(),
firstSeen = min(TimeGenerated),
lastSeen = max(TimeGenerated),
sources = dcount(SourceIp),
fqdnCount = dcount(Fqdn),
sampleFqdns = make_set(Fqdn, 20)
by Action, RuleCollection, Rule, Protocol, DestinationPort
| order by hits desc For a network rule, replace FQDN evidence with observed IPs and ports.
let StartTime = ago(14d);
let RuleName = "allow-shared-egress-443";
AZFWNetworkRule
| where TimeGenerated > StartTime
| where Rule == RuleName
| summarize
hits = count(),
firstSeen = min(TimeGenerated),
lastSeen = max(TimeGenerated),
sources = dcount(SourceIp),
destinations = dcount(DestinationIp),
sampleDestinations = make_set(strcat(DestinationIp, ":", tostring(DestinationPort)), 30)
by Action, RuleCollection, Rule, Protocol
| order by hits desc A rule with no recent hit can be a candidate for expiration, but not immediate deletion if the flow is monthly, batch-driven or tied to disaster recovery. The useful criterion is consistency between business cadence and observed traffic.
Separate valid consumers from noise
A broad rule often serves several consumers without that being explicit. Group observed sources by subnet, workload or environment, then identify usage outside the intended scope.
let StartTime = ago(14d);
let RuleName = "allow-partner-or-temporary-wildcard";
AZFWApplicationRule
| where TimeGenerated > StartTime
| where Rule == RuleName
| summarize hits=count(), lastSeen=max(TimeGenerated), fqdns=make_set(Fqdn, 15) by SourceIp, DestinationPort, Action
| order by hits desc Complete that view with network inventory: which subnet owns each IP, which service uses it, and whether the source should depend on this rule. An unexpected source is not automatically hostile. It may reveal a CI runner, a forgotten batch, an old App Service VNet Integration path, or a route that groups too many workloads behind the same policy.
Check application dependencies before cutting
Tightening should start from the real flow. For an FQDN rule, list the exact domains, aliases, redirects, CDN names, authentication endpoints and secondary calls. For an IP rule, check whether the IP is stable and whether it represents an appliance, private service, partner endpoint or volatile Internet destination.
Dependency to validate
Functional name: partner billing API
FQDN called by workload: api.partner.example
Secondary FQDNs: auth.partner.example, token.partner.example
Ports and protocols: HTTPS 443
Authorized sources: snet-app-prod, snet-jobs-prod
Usage window: continuous, daily batch, monthly batch or DR
Blocking sensitivity: user-facing, back-office, monitoring or recovery
Block tightening when
the test was run from an admin workstation instead of runtime
a secondary FQDN appears without an owner
the rule is used by an infrequent batch that was not replayed
rollback depends on undocumented manual work
an IP destination hides an unidentified name or service Private Endpoint may appear in the analysis if the destination resolves to a private address, but it is not the main lens. The question here is the real Firewall rule scope and whether it can be reversed safely.
Prepare a measurable tightening
Do not replace a broad rule with a narrow one without an observation window. Prepare the diff as a testable hypothesis: which sources remain allowed, which destinations are removed, which temporary rule can receive denials, and which metric triggers rollback.
change:
objective: tighten allow-partner-or-temporary-wildcard
from:
sources: ["10.42.0.0/16"]
destinations: ["*.partner.example"]
ports: [443]
to:
sources: ["10.42.12.0/24", "10.42.18.0/24"]
destinations: ["api.partner.example", "auth.partner.example"]
ports: [443]
validation:
before:
- export current policy
- capture hits by source and destination over 14 days
- replay one call from each critical runtime
during:
- watch denies on removed destinations
- watch application 401, 403, 5xx errors and timeouts
- keep rollback ticket open
success:
- expected hits on the new rule
- no unexpected deny from critical sources
- application probe stays healthy
rollback:
- restore previous rule or collection
- document the missing destination
- reopen only the required scope The plan must be readable by the on-call operator. If only the change author can roll it back, the rule is not ready to be tightened.
Use a canary when risk is high
When Azure Firewall Policy is shared across spokes, a local-looking change can affect neighboring workloads. When possible, start with a canary source: one subnet, one workload, a dedicated collection, or a more specific rule placed before the old broad rule.
Acceptable canary
one critical but well-instrumented source
a representative traffic window
an owner available to test
a prepared policy rollback
Firewall and application logs that can be correlated
Insufficient canary
only a manual curl succeeds
the tested source does not represent the real batch
the old broad rule remains higher priority and hides the new rule
denies are not collected in Log Analytics
validation ignores authentication or callback calls The canary is not there to provide comfort. It is there to find missing dependencies before reducing the global scope.
Decide keep, tighten, expire or roll back
Make the decision explicit. A broad rule can be kept temporarily if the outage risk is higher than the exposure risk, but it must then carry a date, an owner and evidence.
Tighten
Real hits match known sources and destinations
Secondary dependencies are documented
Canary produced the same hits on the new rule
Unexpected denies are zero or explained
Rollback was tested or reviewed
Expire
No hit matches the expected business cycle
Owner confirms the flow is retired
A non-regression test covers the historical workload
Removal is announced with a watch window
Keep temporarily
The rule still serves a useful but poorly documented flow
Outage risk is higher than immediate exposure risk
A split plan is open with date and owner
Logs stay monitored until the next change
Rollback
Denies appear on a known critical flow
A secondary destination is missing from the new scope
The new rule is hidden by unexpected priority
Application errors increase after the change
Observed behavior does not match the plan Avoid the false win where the rule is tightened while a broader exception is added elsewhere. The outcome must compare the before and after scope, not only the name of the modified rule.
Validate after the change
After tightening, read logs in both directions: what now passes through the target rule and what is denied from removed destinations. Add application validation, not only Firewall validation.
let ChangeTime = datetime(2026-07-15T08:00:00Z);
let NewRule = "allow-partner-api-tight";
let OldRule = "allow-partner-or-temporary-wildcard";
AZFWApplicationRule
| where TimeGenerated > ChangeTime
| where Rule in (NewRule, OldRule) or Action == "Deny"
| summarize hits=count(), sources=dcount(SourceIp), fqdns=make_set(Fqdn, 20) by bin(TimeGenerated, 15m), Action, RuleCollection, Rule
| order by TimeGenerated asc Validation is complete when the expected flow uses the target rule, unexpected denials are understood, application metrics remain stable and the rollback path can be closed.
Conclusion
Tightening an Azure Firewall rule is not cosmetic cleanup. It is a production change that can remove a network path used by a workload, batch, callback or recovery procedure.
The right runbook starts from real hits, identifies consumers, qualifies dependencies, prepares a measurable tightening, tests a canary when needed, then decides: tighten, expire, keep temporarily or roll back. A stricter rule only has value when it remains operable, validated and reversible.