VyOSXXXVII · Firewall FundamentalsRule ordering
Rule ordering — sequence numbers, first-match, action accept/drop/reject
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)
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):
- Rule 10 checks: established or related? No — the SYN is NEW. No match.
- 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:
- 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-logalready gives you the logging that used to be the reason for writing one. logon 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
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?
Q2. A WAN-facing firewall rule should use `action reject` to provide fast feedback to legitimate clients.
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.
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.