Chapter 05 · Part II · Defending the network

Firewalls & segmentation

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.

65,535usable port numbers per protocol: TCP and UDP ports are 16-bit numbers, and port 0 is reserved
4,094usable VLAN IDs: the 802.1Q tag has a 12-bit ID, and the values 0 and 4095 are reserved
5 dayshow long Linux connection tracking remembers an idle, established TCP connection by default (432,000 seconds)

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.

Three kinds of doorman

What a firewall can see

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.

KindDecides usingCan stopCannot see
Packet filterThe 5-tuple of each packet, aloneTraffic to ports and hosts that should never be reachable; spoofed source rangesWhether a packet is a reply; anything inside the payload
Stateful firewall5-tuple plus the connection-tracking tableUnsolicited inbound traffic, with one "established, related" rule; out-of-state packetsWhat the conversation says; which application is talking
Proxy / ALGThe application protocol itself (HTTP, DNS, SMTP)Requests to sites or methods the policy forbids; direct connections that skip the proxyEncrypted content it does not terminate
WAFHTTP requests to one applicationMany known attack patterns against that site, as a second layer behind a fixFlaws in the application's logic that look like normal requests
Go deeper: host firewalls and network firewalls are the same idea in two places

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.

Top to bottom

First match wins

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.

PlatformOrderActionsStateWhen nothing matches
iptables / nftablesRules in a chain, top to bottom; first terminal verdict winsAccept, drop, reject, jumpStateful if you match on ct stateThe chain's policy (accept unless you set drop)
AWS security groupNo order: all rules are allow rules, and traffic passes if any rule allows itAllow only; no deny rulesStateful: replies are allowed automaticallyDenied
AWS network ACLNumbered rules, lowest number first; first match winsAllow and denyStateless: replies need their own rulesDenied (a final catch-all rule)
Azure network security groupPriority 100 to 4096, lowest first; processing stops at the first matchAllow and denyStateful (flow records)Built-in default rules at the lowest priority
Google Cloud VPC firewallPriority 0 to 65535, lowest number winsAllow and denyStatefulImplied deny for ingress, implied allow for egress
Go deeper: why allow-only, unordered rules are easier to reason about

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.

Instrument 1

Run packets through a rule table

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.

#ActionDirProtoSourceDestinationDest portStateHitsMove

Send a packet

ReadyPick a packet above and press Send.
Connection-tracking table

    Packet state–
    Matched–
    Rules read–
    Allowed / dropped0 / 0

    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.

    Memory

    State: the note on the door

    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.

    verdict = ct_state ∈ {established, related} ? accept : first_match(rules, packet) ?? policy
    In words: if the packet belongs to (or is an error about) a conversation the firewall already approved, let it through without reading further; otherwise read the rules top to bottom, and if none match, apply the default policy.
    Go deeper: the table is a resource, and it can run out

    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.

    Both directions

    Default deny, in and out

    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.

    Egress filtering at the edge An inside network with a workstation, a server and a camera. Their outbound traffic passes the edge firewall. Web traffic via the proxy and DNS to the internal resolver are allowed. A direct connection from the camera to an unknown internet host is blocked and logged. INSIDE Workstation Server Camera (taken over) Web proxy DNS resolver EDGE allow: proxy → :443 allow: resolver → :53 DENY + LOG: camera → 203.0.113.99 the callback never leaves Internet
    Egress filtering: the camera has no business opening connections to arbitrary internet hosts, so the edge refuses and records the attempt. Allowed outbound traffic is funneled through two services that defenders control and log.
    Go deeper: filter the addresses that cannot be yours (BCP 38)

    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.

    The messages that keep the network working

    Don't block all ICMP

    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.

    Go deeper: IPv6 makes the stateful firewall more important, not less

    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.

    Walls inside the building

    Segmentation: one breach, one room

    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:

    • Users: laptops and desktops. Many outbound needs, few inbound.
    • Servers: file, database and internal application servers. Reachable from users on specific ports; rarely need to start conversations of their own.
    • DMZ: anything the internet must reach, such as a public web server. Reachable from outside, and deliberately unable to reach the internal zones, so a compromised public server is not a doorway inward.
    • IoT: cameras, printers, televisions, building controls. Often unpatchable and the least trustworthy devices you own. Reachable from users where needed; allowed out only to their vendors.
    • Guest: internet only.
    • Management: the jump host administrators work from and the admin interfaces of switches, firewalls and servers. Only this zone may reach admin interfaces, and nothing else may reach into it.

    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.

    Instrument 2

    Measure the blast radius

    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.

    Devices reachable–
    File server–
    Admin interfaces–
    Can call out–

    Zones

    Policy matrix: may the row zone open connections to the column zone?

    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.

    Smaller rooms

    Microsegmentation and the host firewall

    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.

    LayerWall aroundEnforced byStops
    PerimeterThe whole networkEdge firewallUnsolicited traffic from the internet; unexpected outbound traffic
    ZonesGroups of similar devicesVLANs plus a firewall routing between themA compromised camera reaching the file server
    MicrosegmentsOne workload or applicationHost firewalls, hypervisor or cloud security groupsA compromised laptop reaching the laptop next to it
    Keeping the list honest

    Rule hygiene

    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:

    • Every rule carries its reason. Who asked for it, what it is for, and when it should be reviewed or expire. A rule nobody can explain is a rule nobody will dare remove.
    • Find shadowed and redundant rules. A rule that can never match is misleading documentation. Tools can find them mechanically; the Lint button above does it for the toy table.
    • Hunt broad rules. "Any" in the source, destination and port of an allow rule means "this firewall is not filtering here". Wide port ranges open to the whole internet are nearly as bad.
    • Watch hit counts. A rule that has not matched a packet in months is probably no longer needed. Remove it in a controlled way, with logging on, and see whether anything misses it.
    • Log denies. Dropped traffic tells you about misconfigurations and about intruders. Log it, rate-limit the logging so a flood cannot drown the disk, and send it somewhere it will be read (Chapter 17).
    • Review on a schedule. NIST SP 800-41 recommends periodic review of firewall policy and rules, and treats rule changes as something that should go through a defined process.

    Worked example: what Lint finds in the messy rulebook

    #  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.

    Go deeper: how a linter decides one rule shadows another

    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.

    Where this comes from

    Sources