Skip to main content
RunBook Academy

VyOSXXXVII · Firewall FundamentalsRule ordering

Rule ordering — sequence numbers, first-match, action accept/drop/reject

Intermediate⏱ ~20 minvyosconfigureset firewall ipv4 nameset firewall ipv4 input filtercommitsaveshow firewall ipv4 nameshow firewall statisticssudo nft list ruleset

What you'll learn

  • Explain first-match semantics for firewall rules
  • Write rules in production order: most specific first, most general last
  • Distinguish drop from reject and identify when each is appropriate
  • Diagnose the production failure where a too-broad rule shadows a more-specific rule
  • Apply the rule-numbering convention for auditable rule sets

Prerequisites

Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling)

Not yet marked complete on this device.

Firewall rules on VyOS 1.5 LTS evaluate in sequence order. Rule 10 fires before rule 20; rule 20 fires before rule 30. The first rule that matches the packet decides the disposition. Later rules are not consulted.

This first-match semantics is the single most important property of a firewall rule set. The operator who treats rules as “evaluated all at once, take the most permissive match” is operating on a misunderstanding that will produce silent security failures.

This lesson covers the sequence-number convention, the three actions (accept, drop, reject), the production ordering discipline, and the failure modes where a too-broad rule shadows a more-specific rule.

Sequence numbers

Every rule has a numeric sequence number. The operator chooses the number when writing the rule:

set firewall ipv4 name WAN-IN rule 10 action accept
set firewall ipv4 name WAN-IN rule 10 description "Allow established/related"
set firewall ipv4 name WAN-IN rule 10 state established
set firewall ipv4 name WAN-IN rule 10 state related

set firewall ipv4 name WAN-IN rule 20 action accept
set firewall ipv4 name WAN-IN rule 20 description "Allow SSH from jump host"
set firewall ipv4 name WAN-IN rule 20 source address 198.51.100.0/24
set firewall ipv4 name WAN-IN rule 20 destination port 22
set firewall ipv4 name WAN-IN rule 20 protocol tcp
set firewall ipv4 name WAN-IN rule 20 state new

set firewall ipv4 name WAN-IN rule 30 action drop
set firewall ipv4 name WAN-IN rule 30 description "Drop invalid"
set firewall ipv4 name WAN-IN rule 30 state invalid
set firewall ipv4 name WAN-IN rule 30 log

set firewall ipv4 name WAN-IN default-action drop
set firewall ipv4 name WAN-IN default-log

Note the shape of the tree. On VyOS 1.5 a named rule set lives at set firewall ipv4 name <name>, one level deeper than the shallower firewall name <name> path a 1.3-era runbook will show you, and the address family is part of the path — the IPv6 equivalent is a separate rule set under set firewall ipv6 name, not the same one serving both.

Note also what the state matches look like. state established is a leaf on its own. The 1.3 tree hung an enable leaf underneath the state node, and 1.4 removed it; the same happened to logging, which is now the bare leaf log. Both are the kind of change that fails loudly at commit rather than quietly at runtime, which is the good outcome — the reader who pastes a 1.3-era rule set gets an error, not a rule set that silently matches nothing.

The sequence number is not a priority. It is a position. Two rules cannot match the same packet; the first rule by sequence wins.

The production convention is to use a step of 10 (10, 20, 30, 40, …). The step of 10 leaves room for the operator to insert a rule between two existing rules without renumbering the entire set — and renumbering a live rule set is exactly the kind of edit that lets a rule land on the wrong side of a permit.

You do not need a high-numbered catch-all rule. The rule set’s default-action is the catch-all, and it fires after every rule has failed to match. A rule 9999 action drop written next to default-action drop is not an extra safety net; it is a second statement of the same decision, and the two can drift apart in a later edit.

First-match semantics

The first-match semantics is the same as a routing table’s longest-prefix match or an access list’s top-down evaluation. A rule that matches a packet terminates the evaluation. Later rules are not consulted.

flowchart TB
  P["Packet arrives at chain"]
  R10["Rule 10<br/>established/related accept"]
  R20["Rule 20<br/>SSH from jump host accept"]
  R30["Rule 30<br/>INVALID drop"]
  DA["default-action drop<br/>(default-log)"]
  DEC{"Match?"}
  ACC["Action: accept<br/>packet admitted"]
  DROP["Action: drop<br/>packet dropped silently"]
  REJ["Action: reject<br/>ICMP unreachable sent"]

  P --> R10
  R10 -->|"match"| ACC
  R10 -->|"no match"| R20
  R20 -->|"match"| ACC
  R20 -->|"no match"| R30
  R30 -->|"match"| DROP
  R30 -->|"no match"| DA
  DA --> DROP

For a TCP SYN from the jump host (198.51.100.5:50000) to the router (203.0.113.10:22):

  1. Rule 10 checks: established or related? No — the SYN is NEW. No match.
  2. Rule 20 checks: source 198.51.100.0/24, destination port 22? Yes. Match. Action accept. Packet admitted.

For a TCP ACK from the jump host to the router:

  1. Rule 10 checks: established? Yes (conntrack has state). Match. Action accept. Packet admitted. Every later rule is not consulted.

The first-match evaluation means a permissive rule near the top of the chain shadows every rule below it. A rule 10 that accepts all TCP traffic shadows every later TCP rule.

The three actions

A rule can have one of three actions:

  • accept — admit the packet. Conntrack creates an entry (if state new) or updates the existing entry (if state established/related).
  • drop — silently discard the packet. No ICMP error is sent. The sender’s TCP stack will eventually time out the connection (TCP retransmit, typically 75 seconds for the first SYN).
  • reject — discard the packet and send an ICMP unreachable to the sender. The sender’s stack fails the connection immediately.
set firewall ipv4 name WAN-IN rule 30 action drop
set firewall ipv4 name WAN-IN rule 30 description "Drop invalid silently"
set firewall ipv4 name WAN-IN rule 30 state invalid

set firewall ipv4 name WAN-IN rule 40 action reject
set firewall ipv4 name WAN-IN rule 40 description "Reject telnet with ICMP"
set firewall ipv4 name WAN-IN rule 40 destination port 23
set firewall ipv4 name WAN-IN rule 40 protocol tcp

The drop vs reject choice is a tradeoff:

  • drop — silent. The sender does not know why the packet was dropped. The sender’s stack times out. Production default: drop is the safer choice for WAN-facing rules because it denies the attacker feedback about whether the destination exists.
  • reject — noisy. The sender knows immediately that the destination is unreachable. Useful for internal rules where the sender is a legitimate client that benefits from a fast failure. Useful for protocols that cannot tolerate TCP retransmit timeouts (e.g., interactive protocols).

Ordering: most specific first

The production ordering discipline is most specific rule first, most general rule last. The reason: a more general rule near the top of the chain shadows every more-specific rule below it.

# WRONG: general rule first shadows specific rules
set firewall ipv4 name WAN-IN rule 10 action accept
set firewall ipv4 name WAN-IN rule 10 protocol tcp
set firewall ipv4 name WAN-IN rule 10 state new

set firewall ipv4 name WAN-IN rule 20 action drop
set firewall ipv4 name WAN-IN rule 20 description "Block SSH from brute force"
set firewall ipv4 name WAN-IN rule 20 destination port 22
set firewall ipv4 name WAN-IN rule 20 source address 198.51.100.50
set firewall ipv4 name WAN-IN rule 20 protocol tcp
set firewall ipv4 name WAN-IN rule 20 state new

Rule 10 admits every TCP SYN. Rule 20 is never consulted because every TCP SYN matches rule 10 first. The brute-force block is dead.

# RIGHT: specific rule first, general rule last
set firewall ipv4 name WAN-IN rule 10 action drop
set firewall ipv4 name WAN-IN rule 10 description "Block SSH from brute force"
set firewall ipv4 name WAN-IN rule 10 destination port 22
set firewall ipv4 name WAN-IN rule 10 source address 198.51.100.50
set firewall ipv4 name WAN-IN rule 10 protocol tcp
set firewall ipv4 name WAN-IN rule 10 state new

set firewall ipv4 name WAN-IN rule 20 action accept
set firewall ipv4 name WAN-IN rule 20 protocol tcp
set firewall ipv4 name WAN-IN rule 20 state new

Rule 10 blocks the brute-force SSH attempts. Rule 20 admits every other TCP SYN.

The default-action discipline

The default-action is the catch-all. Every chain has one (accept, drop, or reject). The chain’s default-action fires when no rule matches.

set firewall ipv4 name WAN-IN default-action drop
set firewall ipv4 name WAN-IN default-log

The production discipline:

  • Every WAN-facing rule set has default-action drop.
  • Every WAN-facing rule set has default-log, so what the default action catches is visible rather than silent.
  • Do not also write a high-numbered catch-all rule. It states the same decision twice, and default-log already gives you the logging that used to be the reason for writing one.
  • log on an individual rule is the tool for the opposite question — not “what is falling through” but “is this rule firing at all”. Turn it on for the rule you are diagnosing and off again afterwards; a logging rule in the path of real traffic writes at line rate.

There is one thing default-action does not do, and it is the one that catches people migrating from 1.3: a named rule set does not filter anything until a base chain jumps to it. The rule set is a definition; the jump is what puts it in the path.

set firewall ipv4 input filter rule 10 action jump
set firewall ipv4 input filter rule 10 jump-target WAN-IN
set firewall ipv4 input filter rule 10 inbound-interface name eth0

input is the base chain for traffic terminating on the router itself; forward is the one for traffic passing through it. Pick by where the traffic is going, and see vyos-xxxvii-02-zones-and-chains for the full set. Without a jump like this, everything above commits cleanly, reads correctly, and filters nothing — which is the single most common way a migrated 1.3 firewall ends up wide open.

Operational commands

The rule state is exercised through the standard VyOS operational commands:

# All rules in the set, in sequence order, with counters
show firewall ipv4 name WAN-IN

# One rule
show firewall ipv4 name WAN-IN rule 10

# Counters across the whole firewall
show firewall statistics

# The base chain, to confirm something jumps to the rule set
show firewall ipv4 input filter

# The rendered nftables, which is what the kernel is running
sudo nft list ruleset

show firewall ipv4 name WAN-IN lists the rules in sequence order with their hit counters, and the counters are the primary diagnostic. A counter that never advances means the rule is not firing, and there are exactly two reasons for that: nothing matches it, or something above it matched first. Which of the two it is is answered by reading upward, not by re-reading the rule.

The counter increments on match, whatever the action — a drop rule’s counter climbing is the rule working, not the rule failing.

sudo nft list ruleset prints what the kernel is actually running. VyOS renders each named rule set as its own chain and gives it a name derived from the rule-set name, so search the output for WAN-IN rather than guessing at the exact chain and table names, which are VyOS’s to choose and have changed between releases. Reach for this when the VyOS view and the observed behaviour disagree; the kernel’s own ruleset settles it.

sudo nft reset counters zeroes counters, which is useful before a controlled test — but it zeroes them for the whole ruleset, not for one rule set, so anyone else reading counters on that router loses their baseline at the same moment.

Rollback

The rule changes are rolled back the same way as any VyOS configuration change:

# Read the diff before you commit anything
compare

# Apply with an automatic revert if you lose access
commit-confirm 5

# Undo one rule
delete firewall ipv4 name WAN-IN rule 20
commit
save

commit-confirm 5 is not optional discipline on a firewall change; it is the mechanism that gets you back in when the rule you just wrote is the one filtering your own SSH session. Apply, verify from a second session, then confirm.

Whole-configuration rollback restores an entire previous revision — every change committed since goes with it — so it is a deliberate decision about the whole router, not a way to back out one rule.

Two things to check after any rollback of a firewall change. That the rule set still has its default-action, because a rule set whose default action has been removed along with the rules is not a rule set that fails closed. And that the base chain still jumps to it — deleting a rule set’s contents and deleting the jump that reaches it are separate edits, and a rollback that catches one and not the other leaves a firewall that looks configured and is not in the path.

Production discipline

Cross-course references

  • Part XXXVII-01 (XXXVII-VyOS-Firewall / stateful vs stateless) covers the conntrack layer the rules consult.
  • Part XXXVII-02 (XXXVII-VyOS-Firewall / zones and chains) covers the chains the rules live in.
  • Part XXXVII-04 (XXXVII-VyOS-Firewall / state tracking) covers connmark, connlimit, and recent — the conntrack-aware matches that augment rule ordering.
  • Part LII (LII-VyOS-Troubleshoot) covers the diagnostic method for “why is my traffic being admitted/dropped” that uses rule hit counters.

Quiz

Knowledge check · 4 questions

  1. Q1. A chain has rule 10 (accept TCP SYN from any) and rule 20 (drop TCP SYN from 198.51.100.50). A SYN from 198.51.100.50 arrives. What is the disposition?

  2. Q2. A WAN-facing firewall rule should use `action reject` to provide fast feedback to legitimate clients.

  3. Q3. An operator writes three rules in WAN-IN: rule 10 (accept any TCP SYN), rule 20 (accept established/related), rule 30 (drop invalid). The operator's intent is to permit SSH from a jump host (198.51.100.0/24) and HTTPS from any source. But rule 10 admits every TCP SYN. What is the production failure?

    Rule 10: protocol tcp, state new, action accept. Rule 20: state established, action accept. Rule 30: state invalid, action drop. Default-action drop. The chain's intent is to permit SSH from 198.51.100.0/24 and HTTPS from any source, block everything else.

  4. Q4. An operator has a chain with rules at 10, 20, 30 (step of 10). The operator wants to insert a new rule between rule 10 and rule 20. The operator writes `set firewall ipv4 name WAN-IN rule 15 action drop`. Does the rule appear between rule 10 and rule 20? What happens if the operator writes rule 25 instead?

    Existing rules: rule 10 (accept established/related), rule 20 (accept SSH from jump host), rule 30 (drop invalid). The operator wants to add a new drop rule for SSH brute-force from 198.51.100.50. The operator writes rule 15 and then rule 25.

Passing score: 75%. Answers are checked in this browser.