A firewall is a list of decisions about which packets may pass, read from the top, one packet at a time. Most of what goes wrong with firewalls comes from that list: a rule in the wrong place, a rule nobody remembers, a door left open in the outbound direction. And a single firewall at the front door does nothing for the rooms inside. This chapter is about writing the list well, and about building inside walls so one compromised device stays one compromised device.
Common mix-up: NAT is not a firewall. A home router blocks unsolicited inbound traffic because it runs a stateful firewall next to its NAT, not because address translation hides anything. Turn the firewall off and port-forwarding, UPnP and IPv6 (which needs no NAT at all) will cheerfully deliver packets to the inside.
Read first: this chapter assumes how addresses, subnets and switches work, and what NAT does and does not do. If any of that is new, start with IP addresses & subnets, Ethernet, MAC & switches, NAT & port translation and Your home network on networks.jelia.nyc.
Every firewall answers the same question, "should this pass?", but different kinds of firewall look at different amounts of the packet before answering. That decides what they can and cannot protect you from.
A packet filter looks at each packet on its own. It reads the headers: the protocol (TCP, UDP, ICMP), the source and destination addresses, and the source and destination ports. Those five values are called the 5-tuple, and they are the vocabulary of almost every firewall rule ever written. A packet filter is fast and simple, and it has no memory. It cannot tell a reply from a stranger, because it does not remember that anyone asked a question.
A stateful firewall remembers. When a device inside opens a connection, the firewall writes an entry in a connection-tracking table: these two addresses, these two ports, this protocol, this state. Every later packet is checked against the table first. A packet that belongs to a known conversation is established; one that starts a new conversation is new; an error message about a known conversation, such as an ICMP "packet too big", is related. That one idea lets you write "nothing comes in unless we asked for it" as a single rule, and it is why stateful inspection has been the default for decades.
An application-layer gateway goes further and speaks the protocol. A web proxy terminates the connection, reads the HTTP request, and makes its own request onward. A web application firewall (WAF) sits in front of a website and inspects requests for patterns of known attack classes, like the injection bugs in Chapter 13. These can make decisions a packet filter never could, such as "allow this site, but not uploads to it", at the cost of much more work per request.
Here is the catch that shapes modern firewalls: most traffic is now encrypted. A packet filter never needed to see inside, but a gateway that wants to read HTTP sees only TLS records unless it terminates the TLS itself, which means holding a certificate the clients trust (Chapter 3). For traffic you do not terminate, the firewall is back to addresses, ports, timing, and whatever the handshake reveals in the clear, such as the server name a client asks for.
| Kind | Decides using | Can stop | Cannot see |
|---|---|---|---|
| Packet filter | The 5-tuple of each packet, alone | Traffic to ports and hosts that should never be reachable; spoofed source ranges | Whether a packet is a reply; anything inside the payload |
| Stateful firewall | 5-tuple plus the connection-tracking table | Unsolicited inbound traffic, with one "established, related" rule; out-of-state packets | What the conversation says; which application is talking |
| Proxy / ALG | The application protocol itself (HTTP, DNS, SMTP) | Requests to sites or methods the policy forbids; direct connections that skip the proxy | Encrypted content it does not terminate |
| WAF | HTTP requests to one application | Many known attack patterns against that site, as a second layer behind a fix | Flaws in the application's logic that look like normal requests |
A network firewall is a box (or a cloud service) on the path between networks. A host firewall runs on the device itself: nftables on Linux, Windows Defender Firewall, the macOS application firewall. They evaluate the same kind of rules; the difference is what they can see and who can change them.
A network firewall sees only the traffic that crosses it. Two laptops on the same switch talk directly, and the firewall at the edge never sees a single packet of that conversation. A host firewall sees everything that reaches its host, including traffic from the next desk, and it can tie a rule to a program rather than a port. But if an attacker gains administrator rights on that host, they can rewrite its rules. That is why the two are layered rather than chosen between: the network firewall holds even when a host falls, and the host firewall covers the traffic the network firewall never sees.
NIST's guideline on firewalls, SP 800-41 Revision 1, describes both, along with personal firewalls, proxies and the trade-offs of each, and remains the standard reference for writing firewall policy.
Most firewalls read their rules in order and act on the first rule that matches. The packet does not get a vote across all rules; it gets the verdict of the first rule whose conditions it meets, and nothing after that rule is consulted. If no rule matches, the chain's default policy decides.
That sounds obvious until you see what it does to a ruleset that has grown for five years. Suppose someone adds "allow anything to the office subnet" near the top to get a vendor's support session working, intending to remove it later. Every specific deny below it, such as "never let the internet reach the file server's sharing port", is now dead text. It still reads as protection in a review. It never runs. A rule that can never be reached because an earlier rule matches everything it would match is called shadowed.
So the convention in ordered rulesets is: specific rules first, broad rules later, default policy last, and the "established, related" rule at the very top, because the vast majority of packets belong to conversations already approved, and matching them first is both correct and fast.
# illustrative nftables ruleset (example ranges only)
table inet filter {
chain input {
type filter hook input priority 0; policy drop; # default deny
ct state established,related accept # replies and errors about our own traffic
ct state invalid drop # out-of-state packets
iif "lo" accept # the host talking to itself
meta l4proto { icmp, ipv6-icmp } accept # see the ICMP section: don't break PMTUD
tcp dport 443 ip daddr 198.51.100.10 accept # the one public service
}
}
Not every platform uses first-match in a single list, and the differences matter when you move between them. The table below is checked against each vendor's own documentation.
| Platform | Order | Actions | State | When nothing matches |
|---|---|---|---|---|
| iptables / nftables | Rules in a chain, top to bottom; first terminal verdict wins | Accept, drop, reject, jump | Stateful if you match on ct state | The chain's policy (accept unless you set drop) |
| AWS security group | No order: all rules are allow rules, and traffic passes if any rule allows it | Allow only; no deny rules | Stateful: replies are allowed automatically | Denied |
| AWS network ACL | Numbered rules, lowest number first; first match wins | Allow and deny | Stateless: replies need their own rules | Denied (a final catch-all rule) |
| Azure network security group | Priority 100 to 4096, lowest first; processing stops at the first match | Allow and deny | Stateful (flow records) | Built-in default rules at the lowest priority |
| Google Cloud VPC firewall | Priority 0 to 65535, lowest number wins | Allow and deny | Stateful | Implied deny for ingress, implied allow for egress |
An AWS security group is a set, not a list. Because there are only allow rules and anything unmatched is denied, order cannot matter: the result is the union of everything the rules allow. You cannot shadow a deny, because there are no denies. The price is expressiveness: you cannot say "allow this /16 except this one address" in a security group, so designs that need exceptions put them somewhere else, such as a network ACL in front, which is ordered and stateless and brings back every hazard this section describes.
A useful mental model: an ordered list with a default deny and only allow rules behaves like a set. The trouble starts when allows and denies are interleaved, because then position becomes meaning, and position is the one thing a reviewer skimming a long ruleset does not see.
This is a working firewall, run entirely inside this page on made-up traffic. The inside network is 192.0.2.0/24, the public web server lives at 198.51.100.10, and the internet is played by 203.0.113.0/24 (all three are ranges reserved for documentation). Edit any rule, reorder rules with the arrows, pick a packet and send it, and watch the table being read from the top. Turn state tracking on and off to see what connection tracking buys you, then press Lint to have the ruleset checked for shadowed and overly broad rules.
| # | Action | Dir | Proto | Source | Destination | Dest port | State | Hits | Move |
|---|
Things to try: in "Home router", send Laptop browses web and then Send the reply; now switch state tracking off and send the reply again. In "Small office", the same laptop may not browse directly at all, only through the proxy: that is egress filtering. In "Messy rulebook", send File sharing from internet and read which rule let it in, then Lint. In "Stateless ACL", send Unsolicited ACK: it gets in, because the rule that admits replies cannot tell them from strangers.
Connection tracking is the reason one rule, "accept established and related", can replace dozens of rules that try to describe replies. On Linux the tracker is called conntrack, and it labels each packet with one of a small set of states: NEW (the first packet of a conversation the table has not seen), ESTABLISHED (a packet of a conversation that has seen traffic in both directions), RELATED (a new flow tied to an existing one, such as an ICMP error about it), and INVALID (a packet that fits no known conversation and makes no sense as a start, such as a TCP acknowledgment out of nowhere).
Without state, a firewall that wants to let replies back in has to guess what replies look like. A reply to a web request comes from port 443 and arrives at a high, temporary port on the laptop, so the stateless rule ends up as "allow inbound TCP to ports 1024 to 65535". That rule admits the reply. It also admits anything else aimed at those ports, from anywhere, which is most of the port space. You can watch this happen in the instrument's Stateless ACL preset.
Entries do not live forever. Each has a timeout that is refreshed whenever the conversation sends a packet. The Linux defaults are documented in the kernel's conntrack settings: 120 seconds for a TCP connection still waiting for its handshake to complete, 432,000 seconds (five days) for an established TCP connection that has gone quiet, 30 seconds for an unanswered UDP flow, 120 seconds for a UDP flow that has seen replies, and 30 seconds for ICMP. UDP has no handshake and no goodbye, so the firewall's only way to know a UDP "conversation" is over is silence. Press Wait 60 s in the instrument and watch UDP entries disappear while TCP ones remain.
The connection-tracking table has a maximum size (on Linux, nf_conntrack_max), and every tracked flow uses an entry; the kernel documentation notes each connection is stored for both its original and its reply direction. A flood of new connection attempts can fill the table so that legitimate new connections are refused, which is why busy servers sometimes exempt very high-volume, well-understood traffic from tracking, and why stateful devices in front of large sites are sized carefully. How floods are absorbed belongs to Chapter 9.
Long timeouts are a deliberate trade-off. A five-day TCP timeout keeps quiet but legitimate sessions (an idle SSH session, a database connection pool) from breaking; a shorter one frees the table sooner. Middleboxes with short idle timeouts are why some protocols send keepalives.
The most important line in a firewall policy is the last one: what happens to traffic nobody wrote a rule for. NIST SP 800-41 is blunt about it: "Generally, all inbound and outbound traffic not expressly permitted by the firewall policy should be blocked." That practice is called deny by default, and the reasoning is that you can list what your organization needs, but you can never list everything an attacker might try.
Almost everyone applies default deny inbound. Far fewer apply it outbound, and that is where much of the value is. Look at what an attacker needs after getting a foothold on one machine. They need a way to receive instructions, which MITRE ATT&CK files under the tactic Command and Control. They usually want to carry data out: the tactic Exfiltration. Both mean the compromised machine opening connections outward, and an outbound policy of "anything goes" grants both in advance.
Egress filtering means the network lets out only what it expects. Servers usually need almost nothing outbound: updates from a known mirror, a time source, a log collector. Workstations need web traffic, which a well-run network sends through a proxy so there is one place to apply policy and keep logs, and DNS, which should go to the organization's own resolvers rather than anywhere on the internet. An IP camera needs to reach its vendor's cloud and nothing else. Each of those can be written as a short list of allows, with a deny, and a log entry, for everything else.
Egress filtering is not a cure. Determined malware will try to tunnel inside what is allowed, such as HTTPS to an ordinary-looking site. But it removes the easy paths, forces the attacker through the proxy where defenders can see them, and turns a silent callback into a denied-connection log entry. Denied outbound traffic from a server that never talks to the internet is one of the most useful alarms a network can raise; Chapter 17 is about reading those logs.
There is one egress rule every network should have, for the sake of everyone else: do not send packets whose source address is not one of yours. Spoofed source addresses are the raw material of reflection and amplification attacks (Chapter 9), and they are only possible because many networks forward whatever their customers send. RFC 2827, published as Best Current Practice 38, asks networks to filter at their edge so that only packets with legitimate source addresses leave; RFC 3704 (BCP 84) extends the idea to networks with several upstream links.
The inbound mirror is just as cheap: packets arriving from the internet that claim a source address inside your own range, or from ranges that are never routed on the internet, such as the private blocks of RFC 1918, are forged or misrouted and can be dropped at the edge.
ICMP has a bad reputation because "ping" can be used to map which addresses are alive, and some administrators respond by dropping every ICMP message. That breaks things in a way that is miserable to diagnose.
The clearest example is path MTU discovery. Each link on a path has a largest packet it can carry. A sender discovers the smallest one along the path by sending large packets and listening for routers that reply "too big, the limit here is N bytes": an ICMP Destination Unreachable "fragmentation needed" message in IPv4 (RFC 1191), and an ICMPv6 Packet Too Big message in IPv6 (RFC 8201). In IPv6, routers never fragment packets in transit, so those messages are the only way a sender learns to shrink. Drop them, and small packets such as handshakes get through while large ones vanish. Connections open and then hang the moment a large response comes back. This failure is common enough to have a name, a PMTUD black hole.
RFC 4890 gives detailed recommendations for filtering ICMPv6 at firewalls. Its short version: Destination Unreachable, Packet Too Big, Time Exceeded and Parameter Problem error messages must not be dropped for traffic that belongs to established connections, and echo request and reply may be allowed (blocking them buys little). The neighbor-discovery messages that IPv6 uses in place of ARP stay on the local link and must be allowed there for the network to function at all. A stateful firewall makes most of this simple: errors about your own conversations arrive as RELATED and pass the established-related rule.
With IPv4 at home, NAT happens to mean that inside devices have no public address, so unsolicited inbound packets have nowhere obvious to go. With IPv6 there is no NAT: every device has a globally routable address. Whether those devices can be reached from the internet is decided purely by the firewall.
The IETF wrote this down. RFC 4864, "Local Network Protection for IPv6", walks through the protections people attribute to NAT and shows how each is achieved in IPv6 without it, chiefly with a stateful firewall. RFC 6092 recommends that home routers apply "simple security" by default: allow outbound connections and their replies, and filter unsolicited inbound traffic. If you enable IPv6 on a network, check that the firewall rules you wrote for IPv4 have IPv6 equivalents. A ruleset that only mentions IPv4 addresses says nothing about IPv6 traffic, and depending on the default policy that can mean wide open.
Traffic between your network and the internet is called north-south; traffic between devices inside is called east-west. A single edge firewall sees only north-south. In a flat network, where every device sits on one subnet behind that edge, the east-west traffic flows freely: the printer can reach the file server, the guest's phone can reach the camera, and the camera can reach the administrator's laptop. Attackers rely on this. Once inside, they move from machine to machine toward something valuable, which ATT&CK files under Lateral Movement.
Segmentation breaks the flat network into zones and forces traffic between zones through a firewall. On a switched network the usual tool is the VLAN: the IEEE 802.1Q standard adds a small tag to each Ethernet frame carrying a 12-bit VLAN ID, so one physical switch can carry several separate broadcast domains. Each VLAN gets its own subnet, and the router between them becomes a firewall with rules between zones. (Attacks on the switch and VLAN machinery themselves, and the features that stop them, belong to Chapter 6.)
A typical small organization ends up with a handful of zones:
The policy between zones is a matrix: for each pair, may devices in the row zone open connections to devices in the column zone? Writing it out as a matrix makes gaps visible in a way a rule list does not. The instrument below lets you build that matrix and then measures what it is worth: pick a device to compromise and it computes everything an attacker on that device could reach, directly or by hopping through devices that fall in turn.
Assign each device to a zone, tick which zones may open connections to which, then choose a device to compromise (or click one on the map). The map shows every device the attacker could reach and in how many hops, assuming the worst case that each reachable device can be taken over in turn. Ticks are coarse on purpose: a real rule would also narrow by port, but if a zone pair is ticked here, some door between them is open. Start from "Flat network", then try the presets in order and watch the count fall.
Cells marked "used" are the permissions the attacker actually needed to reach what they reached: those are the ones to question first. The diagonal is traffic within a zone; unticking it is a small taste of microsegmentation.
Zones stop traffic between groups, but inside a zone devices still trust each other. If two laptops sit on the same VLAN, the firewall between zones never sees them talk. In the instrument, that is the diagonal of the matrix: leave it ticked and one compromised laptop reaches every other laptop.
Microsegmentation pushes the walls down to individual workloads. Instead of "the server zone may be reached from the user zone", the policy says "this database accepts connections only from these two application servers, on this one port". It is usually enforced close to the workload, by host firewalls managed centrally, by the hypervisor, or by the cloud platform's per-instance security groups, so it applies even to traffic between neighbors on the same subnet. Workstations rarely need to accept connections from other workstations at all, and a host firewall that refuses them removes one of the commonest lateral-movement paths.
Followed to its end, this line of thinking stops trusting network location altogether: every request is checked for who is asking and from what device, wherever it comes from. That is the idea behind zero trust, which Chapter 11 develops. Segmentation is still the foundation it is built on; zero trust adds identity to the question a firewall asks.
| Layer | Wall around | Enforced by | Stops |
|---|---|---|---|
| Perimeter | The whole network | Edge firewall | Unsolicited traffic from the internet; unexpected outbound traffic |
| Zones | Groups of similar devices | VLANs plus a firewall routing between them | A compromised camera reaching the file server |
| Microsegments | One workload or application | Host firewalls, hypervisor or cloud security groups | A compromised laptop reaching the laptop next to it |
Firewall rulesets decay. Rules are added in a hurry to fix an outage and rarely removed, because nobody is sure what will break. After a few years the list is long, partly contradictory, and nobody can say why half of it exists. The fixes are unglamorous and they work:
# action dir proto source destination port note 1 allow in any any any any state est/rel 2 allow in tcp any 192.0.2.0/24 any "temp: vendor support" 3 deny in tcp any 192.0.2.10 445 never reached 4 allow in tcp any 198.51.100.10 443 state new 5 allow in tcp 203.0.113.0/24 198.51.100.10 443 state new 6 allow in udp any any any
Read it as the firewall does. Rule 3 says "never let the internet reach file sharing on 192.0.2.10", but every packet it could match (TCP, from anywhere, to an address inside 192.0.2.0/24, on any port) was already matched by rule 2. So rule 3 is shadowed, and worse, the traffic it was meant to deny is allowed. Rule 5 is redundant: rule 4 already allows that traffic from every source, so 5 changes nothing and only makes the list longer. Rule 6 is an any-to-any allow for UDP, which means this firewall does not filter UDP at all. Rule 2 itself is the root problem: a temporary broad allow that outlived its purpose.
# action dir proto source destination port note 1 allow in any any any any state est/rel 2 allow in tcp any 198.51.100.10 443 state new public web 3 allow in tcp 203.0.113.40 192.0.2.30 443 state new vendor support, expires 2026-11-30 (default policy: deny, logged)
The fix narrows the vendor rule to the one vendor address and the one host it needs, gives it an expiry date, deletes the shadowed and redundant rules, and lets the default deny do the work rule 3 was pretending to do.
Each rule describes a box in a five-dimensional space: direction, protocol, source range, destination range and port range (plus state, if tracked). Rule i shadows a later rule j when i's box contains j's box in every dimension: the same direction, a protocol that is either the same or "any", a source prefix that contains j's, and so on. Containment for address prefixes is a bit test: prefix A contains prefix B when A is no longer than B and the first A-length bits of both addresses agree.
If the two rules have different actions, the shadowing is a real error: j's intent is silently reversed. If they have the same action, j is merely redundant. Partial overlaps, where two boxes intersect without either containing the other, are not errors in themselves, but they are where order matters, and they deserve a comment in the ruleset explaining which rule was meant to win. Firewall analysis research in the early 2000s classified these anomalies formally; commercial and open-source policy analyzers apply the same geometry.
ct state.nf_conntrack_max and the default timeouts quoted above.