FortiGate Policy Bloat: Why It Happens and How to Prevent It
If you’ve managed a FortiGate firewall for more than two years without a structured review process, there’s a high probability your policy table has become a liability. In one mid-sized enterprise environment I audited last year, the firewall had accumulated 847 policies over a six-year period. Of those, 231 (27%) had a hit count of zero — meaning they had never matched a single packet since the last counter reset. Another 94 policies were duplicates or near-duplicates of existing rules, created by different engineers who simply didn’t know what was already there. That firewall was running FortiOS 6.4 on an FG-600E, and policy lookup latency was measurably affecting east-west traffic between VLAN segments.
Policy bloat isn’t a sign of incompetence. It’s a sign of a living network. Engineers respond to incidents, onboard new applications, accommodate temporary access requests that never get cleaned up, and inherit configurations from predecessors who had their own naming conventions — or none at all. The problem compounds over time, and by the time someone notices performance degradation or a failed audit, the cleanup effort feels overwhelming. This post walks through exactly why bloat happens, how to detect and measure it, and the operational processes that prevent it from recurring.
What Policy Bloat Actually Costs You
Before diving into root causes, let’s establish what’s at stake. Policy bloat creates cost across four dimensions that are often underestimated until a serious incident occurs.
Performance degradation: FortiGate performs stateful packet inspection using a top-down sequential policy lookup by default. Every new session must traverse the policy list until a match is found. On a firewall with 800 policies, a packet destined for a service near the bottom of the list triggers 700+ comparisons before matching. With high traffic volume, this accumulates into measurable latency. FortiOS 7.2 introduced policy lookup optimization through ASIC offloading on supported hardware platforms, but the software path is still sequential. I’ve seen environments where trimming from 650 to 290 policies reduced average new session establishment time from 4.2ms to 1.1ms under load.
Security exposure: Bloated policy tables contain shadow rules — policies that are technically active but never reached because a broader rule above them already matches the traffic. Shadow rules create a false sense of security. Engineers believe restrictive policy XYZ is blocking certain traffic, but a permissive any-any rule 50 lines above is silently allowing it. This is one of the most common findings in security audits.
Operational overhead: Troubleshooting becomes exponentially harder when nobody fully understands the policy table. A simple “why can’t user A reach server B” ticket that should take 10 minutes consumes an hour because the engineer has to trace through hundreds of policies, many with no comments or names. This also increases the risk of misconfiguration during incident response, when engineers are under pressure and taking shortcuts.
Compliance failures: PCI-DSS, ISO 27001, and SOC 2 all require documented, reviewed, and justified firewall rules. A bloated policy table with unnamed, uncommented policies is an automatic finding in any serious audit. If you’re pursuing compliance certification, cleanup isn’t optional.
The Five Root Causes of Policy Bloat
1. Temporary Rules That Became Permanent
This is the single largest contributor to bloat in environments I’ve reviewed. The pattern is familiar: a developer needs database access from their workstation to a staging server for testing. An engineer creates a policy, the developer completes their work, and the ticket gets closed. But nobody created a task to remove the firewall rule after the testing period. Multiply this across 50 engineers over 5 years and you have 200+ zombie rules.
FortiOS doesn’t have a native rule expiry mechanism built into the policy object itself (as of 7.4). Some teams use scheduled tasks to disable policies, but this requires discipline that most teams don’t have. The fix is procedural: every temporary policy must have a linked calendar event or ticket with a removal date, and there must be someone accountable for executing the removal.
2. No Naming or Commenting Standards
When policies have no names or comments, future engineers can’t determine intent. If you can’t tell why a policy exists, you can’t safely delete it — so you don’t. The policy stays forever. In environments with consistent naming standards (more on this shortly), engineers can immediately identify policies by age, owner, and purpose, making cleanup decisions fast and defensible.
For a deeper look at why unnamed policies accumulate and how to systematically fix them, see our post on why FortiGate policies often have no name and how to correct the pattern.
3. Application-Driven Policy Creation Without Lifecycle Management
Enterprise applications have lifecycles. When a CRM system gets replaced, the old database access policies, internal load balancer rules, and partner API access policies that supported it should be removed. In practice, the application team decommissions the servers but never files a ticket to clean up firewall rules. The servers go dark, hit counts drop to zero, but the policies remain.
4. Mergers, Acquisitions, and Network Consolidations
When two networks merge, their firewall policies often get combined through a migration process that prioritizes keeping everything working over cleaning up redundancy. I’ve seen merged environments where the same application access was permitted by three different policies — one from each legacy environment and one created during the migration “just to be safe.” After stabilization, nobody went back to remove the redundancies.
5. Fear of Breaking Things
This is the most honest root cause. Engineers know the policy table is messy, but they’re afraid that deleting a policy with zero hit count will break something important that just hasn’t been used recently. A backup server that only runs quarterly? A DR environment that gets tested once a year? A vendor VPN that hasn’t been used since the last engagement? Nobody wants to be the engineer who caused an outage because they deleted a rule that turned out to be critical.
This fear is legitimate, and the solution isn’t courage — it’s data and process. Specifically, it’s using hit count data properly, understanding when counters were last reset, and using a staged disable-before-delete approach.
Measuring Bloat: The CLI Approach
Before you can fix bloat, you need to measure it. Here’s the CLI workflow I use when auditing a FortiGate for the first time.
Step 1: Get total policy count by VDOM
FGT-PROD # config vdom
FGT-PROD (vdom) # edit root
FGT-PROD (root) # end
FGT-PROD # get firewall policy | grep policyid | wc -l
This gives you the raw count. On FortiOS 7.x, you can also use:
FGT-PROD # diagnose firewall iprope show 100004 | head -5
FGT-PROD # get system status | grep policies
Step 2: Export hit counts for all policies
FGT-PROD # diagnose firewall iprope show 100004 0
policy_id=1 hit_count=154823 byte_count=2847561234 ...
policy_id=2 hit_count=0 byte_count=0 ...
policy_id=7 hit_count=0 byte_count=0 ...
The table ID 100004 is the IPv4 policy table for the root VDOM. For other VDOMs, the table ID will differ. Pipe the output to a file and count zero-hit entries:
FGT-PROD # diagnose firewall iprope show 100004 0 | grep "hit_count=0" | wc -l
In a typical bloated environment, I find between 20% and 35% of policies have zero hits. In the worst case I’ve encountered, it was 51% — 347 policies out of 681 had never matched a packet.
Step 3: Check when hit counters were last reset
Hit count data is only meaningful if you know when it was last cleared. Counters reset on reboot or when manually cleared. Check system uptime:
FGT-PROD # get system performance status
Uptime: 847 days, 12:34:56
847 days of uptime means your hit count data covers nearly 2.5 years — that’s highly reliable for identifying truly unused policies. If uptime is only 30 days post-reboot, zero-hit policies need longer observation before deletion.
Step 4: Identify disabled policies
FGT-PROD # show firewall policy | grep -A2 "status disable"
Disabled policies with no comments are prime cleanup candidates. If nobody can explain why a policy is disabled, it’s likely safe to remove after a 30-day notice period.
Step 5: Find policies with “any” in source or destination
FGT-PROD # show firewall policy | grep -B5 -A20 "srcaddr \"all\""
FGT-PROD # show firewall policy | grep -B5 -A20 "dstaddr \"all\""
Policies with “all” source or destination are worth reviewing carefully. Some are legitimate (e.g., an outbound NAT policy). Many are overly permissive rules created during troubleshooting that never got tightened.
A Practical Cleanup Process That Actually Works
The mistake most teams make when approaching cleanup is trying to do everything at once. A 600-policy cleanup done in one change window is a recipe for outages and rollbacks. The approach that works is incremental, data-driven, and reversible.
Phase 1: Disable, Don’t Delete (Weeks 1-4)
For every policy you identify as a cleanup candidate, disable it rather than deleting it. This preserves the configuration while removing the policy from active matching. Monitor for 30 days. If nobody reports an access issue and hit counts on adjacent policies don’t spike in unexpected ways, proceed to deletion.
FGT-PROD # config firewall policy
FGT-PROD (policy) # edit 147
FGT-PROD (policy-147) # set status disable
FGT-PROD (policy-147) # set comments "REVIEW: Zero hit 847 days. Disabled 2024-03-15. Delete after 2024-04-15 if no issues. Owner: netops@company.com"
FGT-PROD (policy-147) # end
Document every disabled policy in a spreadsheet with the disable date, planned delete date, and rationale. This gives you a defensible audit trail and makes the deletion phase mechanical rather than judgment-intensive.
Phase 2: Batch Deletion With Backup Verification
Before any deletion, back up the full configuration:
FGT-PROD # execute backup config ftp 192.168.1.100 backup/ pre-cleanup-20240415.conf
FGT-PROD # execute backup config flash pre-cleanup-20240415
Then delete in batches of no more than 20 policies per change window, with a 48-hour monitoring window between batches. This limits blast radius if something unexpected surfaces.
Phase 3: Consolidation
After removing dead policies, look for consolidation opportunities. Multiple policies with the same source, destination, and action but different services can often be merged into a single policy with multiple services. This requires care — logging settings, schedule objects, and traffic shaping profiles must match for safe consolidation — but the result is a leaner, more readable policy table.
For a comprehensive framework on policy optimization beyond cleanup, the guide on FortiGate policy optimization for network engineers covers ordering, object reuse, and long-term governance in detail.
Prevention: The Operational Practices That Stop Bloat From Recurring
Naming Convention Enforcement
Implement a naming standard and enforce it through peer review. A naming convention I’ve seen work well in practice:
[ENV]-[PURPOSE]-[DIRECTION]-[YYYYMM]-[TICKET]
Examples:
PROD-CRM-APP-INBOUND-202403-CHG0047821
DMZ-VENDOR-CHECKPOINT-OUTBOUND-202311-CHG0039104
TEMP-DEV-TESTING-202404-CHG0052001-EXPIRES20240531
The ticket number is critical — it provides a link back to the business justification. The YYYYMM stamp makes age immediately visible. TEMP prefix triggers automatic review queue enrollment.
Quarterly Hit Count Review
Schedule a recurring quarterly task to export hit count data and flag policies with zero hits over the review period. This can be automated using the FortiGate REST API:
GET /api/v2/monitor/firewall/policy?vdom=root&policyid=all
Authorization: Bearer <api-token>
Accept: application/json
The response includes hit_count, bytes, and packets for each policy. Parse this into a spreadsheet or dashboard, and automatically generate tickets for any policy with zero hits for 90+ days. Assign those tickets to policy owners for review and disposition.
Expiry-Linked Policy Creation for Temporary Access
Every policy created for temporary access must have:
- A comment with the expiry date and responsible owner
- A corresponding calendar event or automated ticket scheduled for the expiry date
- A TEMP prefix in the policy name
- A schedule object if the access is truly time-limited (e.g., vendor access only during business hours)
FGT-PROD # config firewall schedule onetime
FGT-PROD (onetime) # edit "VENDOR-PATCHING-20240415"
FGT-PROD (VENDOR-PATCHING-20240415) # set start 00:00 2024/04/15
FGT-PROD (VENDOR-PATCHING-20240415) # set end 23:59 2024/04/15
FGT-PROD (VENDOR-PATCHING-20240415) # end
FGT-PROD # config firewall policy
FGT-PROD (policy) # edit 0
FGT-PROD (policy-new) # set name "TEMP-VENDOR-PATCHING-20240415-CHG0055102"
FGT-PROD (policy-new) # set schedule "VENDOR-PATCHING-20240415"
FGT-PROD (policy-new) # set comments "Temp vendor access for patch night. Owner: ops@company.com. Auto-expires 20240415."
FGT-PROD (policy-new) # end
With a one-time schedule, the policy automatically stops matching after the window passes. You still need to delete it eventually, but the security risk is eliminated automatically.
Change Management Integration
Require a valid change ticket number in every policy comment. This creates a two-way traceability link: you can look up a policy and find its business justification, or look up a change ticket and verify the firewall was updated correctly. This discipline pays off enormously during audits and incident investigations.
Tools That Accelerate Bloat Detection
Manual CLI review works, but it doesn’t scale well for environments with 500+ policies across multiple VDOMs or multiple FortiGate units. For larger environments, purpose-built analysis tools can compress what would be an 8-hour manual review into 45 minutes by automatically flagging unused policies, shadow rules, duplicate objects, and optimization opportunities.
If you’re evaluating tooling for policy analysis, the APO Tool for FortiGate firewall policy analysis is worth examining. It ingests the configuration file directly — no SNMP or API access required — and produces a prioritized cleanup report. In a 600-policy environment, it identified 89 zero-hit policies (14.8%) and 23 shadow rule conflicts that the manual review had missed, completing the analysis in under 3 minutes.
Benchmarks: What “Healthy” Looks Like
| Metric | Bloated Environment | Healthy Environment |
|---|---|---|
| Zero-hit policies (90-day window) | 25–50% | <5% |
| Unnamed policies | 40–80% | <2% |
| Policies with no comments | 60–90% | <10% |
| Shadow rule conflicts | 5–15% | 0% |
| Policies with “any” source or destination | 15–30% | <3% |
| Average policy review time (full table) | 6–10 hours | 30–60 minutes |
A Cleanup Checklist for Your Next Review
- Export full policy table and hit counts via CLI or API
- Note system uptime to assess hit count reliability window
- Flag all policies with zero hits for full uptime period
- Flag all disabled policies with no explanatory comments
- Flag all policies without names or comments
- Flag all policies with “all” source or destination (review for over-permission)
- Run shadow rule analysis (manually or via tool)
- For each flagged policy: research ticket history, consult owner, disposition
- Disable cleanup candidates, set 30-day review window
- Back up config before any deletion batch
- Delete in batches of 20 or fewer with 48-hour monitoring windows
- Document all deletions with policy ID, name, rationale, and approver
- Schedule recurring quarterly hit count review
- Update naming convention documentation and enforce on next change
Conclusion
Policy bloat is predictable, measurable, and preventable. The engineers who successfully manage it aren’t smarter or more disciplined in general — they’ve built specific operational habits and tooling that make the right behavior easy. Hit count monitoring, naming conventions, expiry-linked policy creation, and incremental cleanup processes are all achievable with tools that are already available in FortiOS. The cost of not doing this — degraded performance, shadow rule security exposure, audit findings, and operational inefficiency — is consistently higher than the cost of building these habits.
Start with measurement. Export your hit counts today, check your uptime, and calculate your zero-hit percentage. That number alone will tell you whether you have a bloat problem worth addressing — and by how much.

