Every firewall rule is a decision someone made, at a moment, with incomplete information. Six months later the decision is still in force and the reasoning is gone. That is how a rulebase becomes archaeology.
The discipline is simple and mostly clerical: name the rule for its intent, write down why, test it before and after, and review it on a schedule.
Name for intent, not protocol
A rule called allow-tcp-443 tells you nothing. A rule called student-to-internet tells you what was
decided.
<source>-to-<destination>-<service>
student-to-wan
staff-to-infra-dns
campus-to-district-print
Add the ticket or date in a comment field if the platform has one, and keep the detailed rationale in the documentation, not in the rule name. Names are for scanning; documentation is for understanding.
Order by specificity
Rules should read from most specific to most general within a zone pair:
- Explicit denies for known-bad or known-unwanted flows.
- Specific allows (one host, one service).
- Broad allows (a whole zone to a service group).
- The default deny, implicit or explicit.
The first match wins, so this ordering makes each rule reachable and each exception visible. The inverse ordering — a broad allow near the top — is how a rulebase becomes unfalsifiable: everything works, so nobody notices that half the rules never fire.
If a rule has zero hits, it is either new, wrong, or shadowed
A newly added rule has zero hits and that is expected. A rule that has been in place for a month with zero hits is telling you something: the traffic never reaches it (shadowed), or the service is unused (removable). Both are worth knowing.
Least privilege, expressed as a table
Write the intent as a table before touching the firewall:
| From | To | Service | Why | Ticket |
|---|---|---|---|---|
| Student | Infrastructure | DNS 53 | Name resolution | CHG-1042 |
| Student | Printers | 9100, 631 | Lab printing | CHG-1042 |
| Student | WAN | Any | Internet, filtered at DNS | CHG-1042 |
| Student | Everything else | Any | Deny | — |
Then implement it literally. The table is also the review artifact: a year later, someone can compare the table to the rulebase and see drift immediately.
Test before and after
| Test | Before | After | Proves |
|---|---|---|---|
| Browse from student VLAN | Works | Works | Did not break the default path |
| Resolve internal name | Works | Works | DNS rule still correct |
| Print from lab | Works | Works | Printing rule still correct |
| Reach a server from student VLAN | Blocked | Blocked | Segmentation actually applied |
| Reach the internet from guest | Works | Works | Guest path untouched |
The fourth test is the one people skip. A rulebase that permits everything passes the first three tests and fails the entire point of the change.
Change control that fits a one-person team
- One change window, after 17:00, as the district already uses.
- Write the intended end state, not the clicks.
- Take a config backup before the change.
- Apply, test the table above, and record the result.
- Keep the old rule (disabled) for one week, then delete it.
That last step matters: disabling rather than deleting gives you a one-click rollback during the week when problems surface.
Worked example: the new campus
Six rules for a new elementary, expressed as intent:
| # | Rule | Purpose |
|---|---|---|
| 1 | campus-to-district-dns | AD and name resolution |
| 2 | campus-to-district-print | Printing |
| 3 | campus-to-wan | Internet, filtered |
| 4 | district-to-campus-mgmt | Monitoring and management |
| 5 | campus-iot-to-wan | HVAC and cameras to vendor cloud |
| 6 | campus-deny-rest | Everything else |
Apply them, then test from a student device, a staff device, and an IoT controller. If the IoT controller can reach a student device, rule 6 is not doing its job.
What breaks in production
- The permanent temporary rule.
allow-any-any-testfrom a troubleshooting session, still there two years later. - Rules without owners. Nobody knows what depends on them, so nobody removes them.
- Policy drift. The documentation says one thing; the firewall says another; nobody has compared them.
- Over-broad service groups. "Allow web" that includes 80, 443, 8080, and 8443 because someone was in a hurry.
- No default deny. The rulebase ends with allow-all, so every mistake is permissive.
Checklist
- Every rule name states source, destination, and intent.
- Rules are ordered specific to general, with an explicit deny at the end.
- The intent table exists in the documentation and matches the live rulebase.
- Each change has a before/after test, including a negative test.
- Decommissioned rules are disabled for a week, then deleted.
- The rulebase is reviewed quarterly, and the review is recorded.
Next
Writing policy is the easy half. Continue with Auditing a Live Rulebase, where hit counters tell you what the rules actually do.