Field Guide · Networking
A firewall rule is a predicate over a packet — and the hard part is knowing which packet it sees. By the time a rule runs, NAT may have rewritten the addresses, a proxy or load balancer may have replaced the client, and the order of the rules above it decides whether it runs at all. This guide covers rule anatomy, stateful matching, and the three things that most often make a "correct" rule do nothing: pipeline position, address translation, and intermediaries.
Writing a firewall rule is the easy part; predicting what it matches is the skill. A rule that reads exactly right in the config can still pass traffic you meant to block or block traffic you meant to pass — because it matched a translated address, ran after the packet had already been accepted, or keyed on a client IP the server never actually sees. Everything here is about closing the gap between what a rule says and what it does.
A rule is a match (a predicate over the packet or flow) paired with an action (the verdict). The match fields are the connection's five-tuple — source address, destination address, protocol, source port, destination port — usually plus the interface or zone and the direction, and on an application-aware (next-generation) firewall the user identity or application as well. The action is typically allow (accept), drop (discard silently), reject (discard with an ICMP or TCP-reset notice), and often log.
The rule you don't see matters most: nearly every firewall ends with an implicit default deny. Anything no rule explicitly permitted is dropped. That is why a ruleset is a list of exceptions to "no," and why an omitted rule fails closed rather than open.
Drop is silent: the sender waits for a timeout, which slows a port scanner but also slows a legitimate client hitting the wrong port, and hides the firewall's presence. Reject answers immediately, which is friendlier to legitimate clients and faster to diagnose but confirms the host exists and is filtering. Neither is "more secure" in the abstract — drop trades diagnosability for a little scan-slowing obscurity; reject trades that obscurity for faster failure. Choose against the behaviour you actually care about.
A stateless filter (a classic ACL) judges each packet in isolation against the rules, with no memory of what came before. It's fast and simple, but it means you must explicitly permit the return direction — and since replies come back to a client's ephemeral source port (IANA 49152–65535, though Linux and Windows use different practical ranges), your return rule has to permit that whole range, which is coarse.
A stateful firewall tracks connections in a table keyed on the five-tuple, with each flow in a state such as new, established, or related. You permit the forward direction once; reply packets match the established state and are allowed automatically, and any NAT applied to the flow is reversed on the replies without a second rule. This is both safer (return traffic must belong to a connection you already permitted) and simpler to write.
It addresses one specific threat: unsolicited inbound packets forged to look like replies are rejected because they match no tracked connection. It does not make an allowed flow safe — an attack carried inside an established, permitted connection sails through. And it adds its own failure modes: the state table is finite, so a flood of half-open connections can exhaust it (a denial-of-service vector), and asymmetric routing — replies returning via a different firewall that never saw the outbound packet — breaks state tracking entirely, dropping legitimate traffic that looks stateless to the second box.
Most firewalls evaluate first-match-wins: rules are read top to bottom and the first one whose match succeeds decides the verdict — evaluation stops there. That single fact makes order semantic, not cosmetic. A broad allow placed above a narrower deny means the deny never executes; it is shadowed, dead weight that looks like protection in the config and provides none.
Two habits follow. Order from specific to general, with denies above the broad allows they carve exceptions into. And place a log rule immediately before the final default deny: it shows you exactly what the ruleset is dropping, which is how you find both attacks and your own over-tight rules. One caveat worth verifying per platform: not every firewall is strict first-match — some next-generation policy engines evaluate a most-specific match or an ordered named-policy set, so confirm your platform's match algorithm rather than assuming top-to-bottom everywhere.
A rule doesn't just match a packet; it matches a packet at a place, in a direction. Two models dominate. An interface ACL is bound to one interface and one direction (inbound or outbound) — it only ever sees traffic crossing that interface that way. A zone-based firewall groups interfaces into named zones (say inside, dmz, outside) and writes policy for traffic moving from one zone to another, so the same rule covers every interface in the zone and intra-zone traffic is often allowed by default. The trap is identical in both: a correct rule placed on the wrong interface, the wrong direction, or the wrong zone-pair simply never sees the packet and silently does nothing.
This is the spatial companion to the pipeline question in the next section: where the rule sits (interface, zone-pair, direction) decides whether the packet reaches it at all, and when it sits (before or after translation) decides which addresses it sees. Both have to be right, and a rule can be perfectly written yet wrong on either axis.
A firewall never matches "the packet" as a fixed object — it matches whatever the packet looks like at the point the rule runs. Address translation and the routing decision happen at defined stages, so a rule placed before translation sees the original addresses and one placed after sees the rewritten ones. Get the stage wrong and a perfectly-worded rule matches the wrong thing.
Linux netfilter makes the stages explicit, and the model generalises. For traffic forwarded through the box, the order is: PREROUTING (connection tracking runs; destination NAT is applied here) → the routing decision → FORWARD (the packet-filtering verdict) → POSTROUTING (source NAT is applied here). Because DNAT happens in PREROUTING — before the filter — the FORWARD rule sees the post-DNAT (real internal) destination; because SNAT happens in POSTROUTING — after the filter — that same rule still sees the original, pre-SNAT source.
The routing decision also sits after DNAT, which is what lets a port-forward or transparent proxy change where a packet goes — and why the same box can forward one rewritten packet while delivering another to a local socket.
The pipeline turns into three concrete traps. Port-forward / DNAT: if you publish a public address that DNATs to an internal host, the filter rule must permit the internal (post-DNAT) destination, because translation already happened upstream of the verdict — a rule written against the public IP silently never matches. SNAT / masquerade: once outbound traffic is source-translated, the destination and every device past the NAT see a single shared address, so filtering and — just as important — log attribution downstream cannot tell your internal hosts apart. Hairpin (NAT loopback): an internal client that reaches its own service by the public name needs the NAT device to translate and reflect the traffic back inside; without explicit hairpin support it simply fails, which is a classic "works from outside, not from inside" ticket.
On a stateful box you permit the forward direction only. Connection tracking rewrites replies in the reverse direction automatically — a DNAT on the request becomes the matching SNAT on the response without a rule. Adding an explicit reverse-NAT or return-allow rule is redundant at best and, if it's broader than the tracked flow, a hole. And the security caveat the addressing guides make still holds here: NAT is address translation, not a policy control — its "nothing gets in" side effect is incidental, and the stateful filter is what actually enforces the boundary, against unsolicited inbound only.
A proxy or load balancer usually terminates the client's connection and opens a fresh one to the backend. So the backend — and any firewall in front of it — sees the intermediary's source address, not the client's. Every rule or log that keys on the real client IP is now matching the load balancer instead, which quietly defeats per-client allow-lists and makes forensic attribution point at your own infrastructure.
The fixes are all about carrying identity across the hop deliberately: read X-Forwarded-For at Layer 7, the PROXY protocol preamble at Layer 4, or a direct-server-return / transparent mode that preserves the source address — each only trustworthy when the backend is configured to accept it solely from your own intermediary. Enforce per-client decisions at the tier that still sees the client, and correlate logs through the forwarded identity or the balancer's own records, or the actual actor is invisible to you.
Everything so far matches on fields that are exactly what they say they are: a port is a number, an address is an address, and the match is deterministic. Next-generation firewalls add matches on the application (app-ID) and the user (mapping a source address to an identity from a directory feed). These are powerful — policy in terms of "the finance group may use the ERP app" reads far better than port numbers — but they are inferential, and that changes their reliability class.
App-ID guesses the application by inspecting payload and behaviour; user-ID trusts an address-to-user mapping that can be stale the moment a DHCP lease or a shared host changes hands. Both can be evaded (traffic shaped to look like a permitted app), misidentified (a benign flow tagged as the wrong app, or identified only after several packets have already passed), and desynchronised (the user mapping wrong). In other words an app-ID or user rule carries the same false-positive and false-negative exposure as any detection signature, which an L3/L4 rule does not — so it should be validated against real traffic and treated as a fallible layer, not a fact.
Most traffic is TLS, so to identify or inspect it the firewall must terminate and re-encrypt the session as a deliberate man-in-the-middle, using a CA the endpoints are configured to trust. That addresses a real threat — malware and exfiltration hidden inside encrypted flows — but its residual cost is concrete: certificate-pinned and mutually-authenticated apps break, users' encrypted traffic is now readable at the box, and the inspection device becomes both a high-value target and a single point of failure. Decide it against a stated threat, exempt what must stay end-to-end, and keep the deterministic L3/L4 rules as the backbone that still holds when the L7 inference is wrong.
A source address is just a claim in a header, and by default a firewall believes it. Ingress filtering — BCP 38 (RFC 2827) — is the discipline of dropping packets whose source could not legitimately arrive on the interface they came in on: on an internet-facing interface, deny inbound packets that claim your own internal or private (RFC 1918) source addresses and known bogons; at an edge serving a customer, permit only that customer's allocated prefixes. The threat it addresses is source spoofing — the enabler of reflection/amplification DDoS and of forged management access — and its residual is honest: it cannot stop spoofing within an allowed prefix, and it only fully works when everyone does it.
uRPF (unicast Reverse Path Forwarding) automates this, and BCP 84 (RFC 3704) describes its modes. Strict uRPF drops a packet unless the best route back to its source points out the interface it arrived on — effective on a single-homed edge, but it drops legitimate traffic under asymmetric routing, the same failure that breaks stateful tracking. Loose uRPF only checks that the source is routable at all — good for discarding bogons, weak against targeted spoofing. Feasible-path sits between them. Match the mode to the topology, and never enable strict mode on a link you know is asymmetric.
Reflexively dropping all ICMP is a classic self-inflicted fault: Path MTU Discovery depends on the ICMP fragmentation-needed message (ICMPv6 packet-too-big), so blocking it produces the MTU black hole where small packets pass and large ones vanish. Permit the messages that keep connectivity working and rate-limit rather than drop wholesale. In IPv6 this is not optional: ICMPv6 also carries Neighbor Discovery (the replacement for ARP), so a rule that blocks ICMPv6 broadly breaks IPv6 outright.
IPv6 also removes the accidental cover of NAT. With globally-routable hosts and typically no translation, the firewall is unambiguously the boundary — and source validation matters more, not less, precisely because there is no NAT to obscure addresses. The recurring dual-stack trap is a carefully-hardened IPv4 ruleset with the IPv6 path left wide open: write parallel v6 rules, confirm both address families are covered, and remember that "the v4 rules are tight" says nothing about v6.
Inbound default-deny is the norm: permit only the services you mean to expose, and everything else falls to the implicit deny. That addresses unsolicited inbound access — and nothing more. The services you do permit are still exposed, and a permitted port carries whatever payload someone sends to it: allowed is not safe, only reachable-by-design.
The underused half is egress filtering — default-deny outbound, permitting only known destinations and ports. Its threat model is the part of an attack that happens after a foothold: data exfiltration, command-and-control callbacks, and lateral movement to other networks. It is genuinely valuable and genuinely partial — a capable adversary tunnels over the channels you must leave open (DNS, HTTPS to reputable destinations), so egress rules raise the cost and narrow the options rather than closing them, and they earn their keep only when paired with logging and inspection of what those allowed channels actually carry.
Rules also have a lifecycle, and a ruleset rots without one. Every rule should carry an owner, a reason or ticket, and ideally an expiry — because the "temporary" any-any exception opened at 2 a.m. is the one still there two years later. Use object groups and named aliases so intent survives address changes, review and recertify rules on a schedule, and delete the shadowed and orphaned rules the audit turns up rather than leaving dead policy that misleads the next reader. A rule nobody can explain is a rule nobody can safely keep.
A stateful firewall in a high-availability pair must synchronise its connection table to its peer, or every established flow drops the instant it fails over — the session state, not just the config, has to replicate. And decide deliberately what happens when the firewall itself fails or is overwhelmed: fail-closed protects confidentiality and integrity at the cost of availability (traffic stops), fail-open preserves availability at the cost of the control (traffic passes unfiltered). Neither is universally right; choose against the threat and the service, and make the choice explicit rather than discovering your device's default during an outage.
The classic self-inflicted outage is a deny rule that severs your own management path. Before any tightening on a remote device: confirm a rule permitting your management access sits ahead of the new deny, keep out-of-band access, and arm an automatic rollback (commit-confirmed, reload in) so a mistake self-heals. Then change one rule at a time and re-test from the client's real vantage — not from the jump box, whose path and identity differ. A rule verified only from the wrong source is a rule you haven't verified. Where the platform offers a policy simulator or packet-trace/what-if tool, confirm the intended match before committing.
Rule = match (5-tuple + zone/direction) → action (allow / drop / reject / log), then an implicit default deny. Stateful: permit the forward direction once; replies auto-match established. A rule on the wrong interface, direction, or zone-pair never matches. Residual: state exhaustion, asymmetric-routing breakage, allowed ≠ safe.
Usually first-match-wins (verify per platform) — specific and deny rules above broad allows, log before the default deny. The filter sees the post-DNAT destination, pre-SNAT source: for a port-forward, permit the translated internal address; never hand-write the reply rule.
Behind a proxy/LB the backend sees the intermediary's IP; carry the client via XFF / PROXY protocol. App-ID and user-ID are inferential — evadable, misidentifiable, FP/FN-prone — and seeing L7 means decrypting TLS; keep L3/L4 as the deterministic backbone.
Ingress-filter spoofed sources (BCP 38); uRPF strict breaks on asymmetry. Don't blanket-block ICMP/ICMPv6 (PMTUD, NDP). Default-deny egress too; write parallel IPv6 rules. Give rules an owner and expiry; sync HA state; don't lock yourself out; test from the real vantage.
Verified for this guide against current documentation: the netfilter pipeline order — PREROUTING (conntrack, DNAT) → routing decision → FORWARD (filter) → POSTROUTING (SNAT), replies auto-reversed by connection tracking — and the ingress-filtering standards, BCP 38 (RFC 2827) for source-address validation and BCP 84 (RFC 3704) for its uRPF modes (strict / feasible-path / loose). Other firewalls name the pipeline stages differently and some do not use strict first-match, so confirm both pipeline position and match algorithm against your platform; rule syntax shown is illustrative, not any one vendor's, and ICMP message types are named by function rather than number — check your platform's exact filters. No unqualified assurance is intended: each control is tied to a specific threat and leaves residual risk — default-deny stops unsolicited inbound but not attacks over permitted ports; stateful filtering rejects forged replies but not payloads inside established flows; app-ID and user-ID are heuristic and carry false-positive/false-negative exposure; TLS inspection reads encrypted traffic at the cost of breaking pinned apps and making the box a target; egress filtering raises the cost of exfiltration and C2 without closing tunnelable channels; ingress filtering cannot stop spoofing within an allowed prefix; and NAT is address translation, not a policy control. Pairs with Networking (NAT, ports, IPv6), Subnetting & CIDR (private range is scope, not a boundary), the Misconceptions guide (NAT ≠ firewall, reachable ≠ allowed), Zero Trust (reachability ≠ authorization), Sage-Advice (one change, reversibly; don't lock yourself out), and the Diagnostic Command Card (port, connection, and MTU checks). Terms are in the Glossary; 00 · Start Here indexes the set.