Skip to main content
RunBook Academy

VyOSXXXVII · Firewall FundamentalsDefault deny

Default deny — WAN-IN posture, established accept, INVALID log, the production reference architecture

Advanced⏱ ~24 minvyosconfigureset firewall ipv4 nameset firewall zonecommitsaveshow firewall ipv4 nameshow firewall statisticsshow log firewallsudo nft list ruleset

What you'll learn

  • Configure the canonical WAN-IN chain with default deny and explicit permits
  • Choose between the global state-policy and per-chain established/related rules
  • Log INVALID packets to surface scanners and misconfigured peers
  • Layer rate limiting and brute-force protection on top of the base chain
  • Recognise the production anti-patterns (implicit default accept, missing log, shadowed deny)

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.

The default-deny firewall is the production reference architecture. Every WAN-facing chain on every production router starts from this template; the operator customises it by adding the explicit permits for the legitimate inbound services.

The template is small, ordered, and auditable. The operator who starts from a known-good template makes fewer mistakes than the operator who designs the firewall from scratch. The discipline: the default-deny template is the production default, and the deviations from it must be documented.

This lesson covers the canonical WAN-IN chain on VyOS 1.5, the two places established/related handling can live, the INVALID log that surfaces scanners, the rate-limiting and brute-force protection layered on top, and the production anti-patterns the operator must avoid.

The canonical WAN-IN chain

The canonical WAN-IN chain filters traffic from the WAN to the LAN or DMZ. It is the most security-sensitive chain on the router; the consequences of a permissive rule are immediate and severe.

On VyOS 1.5 the chain is a custom chain — firewall ipv4 name WAN-IN — and it does nothing until a base chain sends traffic to it. There are two ways to arrange that hand-off. The direct one is a jump from forward filter:

set firewall ipv4 forward filter default-action 'drop'
set firewall ipv4 forward filter rule 100 action 'jump'
set firewall ipv4 forward filter rule 100 jump-target 'WAN-IN'
set firewall ipv4 forward filter rule 100 inbound-interface name 'eth0'

The zone-based one groups interfaces first and applies rule-sets to zone pairs:

set firewall zone WAN interface 'eth0'
set firewall zone WAN default-action 'drop'
set firewall zone LAN interface 'eth1'
set firewall zone LAN default-action 'drop'
set firewall zone LAN from WAN firewall name 'WAN-IN'

Either way, the chain itself is the same:

# WAN-IN chain — canonical default-deny posture
set firewall ipv4 name WAN-IN description 'Internet to LAN'
set firewall ipv4 name WAN-IN default-action 'drop'
set firewall ipv4 name WAN-IN default-log

# Rule 10: established/related accept (fast-path return traffic)
set firewall ipv4 name WAN-IN rule 10 action 'accept'
set firewall ipv4 name WAN-IN rule 10 description 'Established/related accept'
set firewall ipv4 name WAN-IN rule 10 state 'established'
set firewall ipv4 name WAN-IN rule 10 state 'related'

# Rule 20: INVALID drop with log (surface scanners and broken peers)
set firewall ipv4 name WAN-IN rule 20 action 'drop'
set firewall ipv4 name WAN-IN rule 20 description 'Drop invalid (log)'
set firewall ipv4 name WAN-IN rule 20 state 'invalid'
set firewall ipv4 name WAN-IN rule 20 log

# Rule 25: SSH brute-force drop — must precede the SSH permit
set firewall ipv4 name WAN-IN rule 25 action 'drop'
set firewall ipv4 name WAN-IN rule 25 description 'SSH brute-force'
set firewall ipv4 name WAN-IN rule 25 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 25 destination port '22'
set firewall ipv4 name WAN-IN rule 25 state 'new'
set firewall ipv4 name WAN-IN rule 25 recent count '5'
set firewall ipv4 name WAN-IN rule 25 recent time 'minute'
set firewall ipv4 name WAN-IN rule 25 log

# Rule 30: SSH from jump host only
set firewall ipv4 name WAN-IN rule 30 action 'accept'
set firewall ipv4 name WAN-IN rule 30 description 'SSH from jump host'
set firewall ipv4 name WAN-IN rule 30 source address '198.51.100.0/24'
set firewall ipv4 name WAN-IN rule 30 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 30 destination port '22'
set firewall ipv4 name WAN-IN rule 30 state 'new'

# Rule 40: HTTPS to web server
set firewall ipv4 name WAN-IN rule 40 action 'accept'
set firewall ipv4 name WAN-IN rule 40 description 'HTTPS to web server'
set firewall ipv4 name WAN-IN rule 40 destination address '203.0.113.10'
set firewall ipv4 name WAN-IN rule 40 destination port '443'
set firewall ipv4 name WAN-IN rule 40 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 40 state 'new'

# Rule 50: SMTP to mail server
set firewall ipv4 name WAN-IN rule 50 action 'accept'
set firewall ipv4 name WAN-IN rule 50 description 'SMTP to mail server'
set firewall ipv4 name WAN-IN rule 50 destination address '203.0.113.20'
set firewall ipv4 name WAN-IN rule 50 destination port '25'
set firewall ipv4 name WAN-IN rule 50 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 50 state 'new'

Five things carry the design:

  1. Established/related accept (rule 10) — return traffic is admitted on one rule instead of walking the chain.
  2. INVALID drop with log (rule 20) — the log node writes each dropped packet to the firewall log, which is what makes scanning visible.
  3. Brute-force drop (rule 25) — placed before the SSH permit, because first-match wins and a drop rule after an accept never fires.
  4. Explicit permits (rules 30-50) — each has a protocol, a specific destination, a state, and a source restriction wherever one is possible.
  5. default-action drop with default-log — the safety net, and the audit trail for everything that reaches it.

Note what is not here: a rule 9999 “catch-all deny for documentation”. On 1.5 the custom chain’s default-action is already drop and default-log already logs it. A duplicate explicit rule adds a second counter that will disagree with the first one, and one more thing to keep in sync.

flowchart TB
  P["WAN packet arrives on eth0"]
  BASE["forward filter rule 100<br/>inbound-interface eth0 → jump WAN-IN"]
  R10["Rule 10<br/>established/related accept"]
  R20["Rule 20<br/>INVALID drop+log"]
  R25["Rule 25<br/>SSH brute-force drop"]
  R30["Rule 30<br/>SSH from jump host"]
  R40["Rule 40<br/>HTTPS to web"]
  R50["Rule 50<br/>SMTP to mail"]
  DA["WAN-IN default-action drop + default-log"]
  ACC["Admitted"]
  DROP["Dropped (logged)"]

  P --> BASE
  BASE --> R10
  R10 -->|"match"| ACC
  R10 -->|"no match"| R20
  R20 -->|"match"| DROP
  R20 -->|"no match"| R25
  R25 -->|"match"| DROP
  R25 -->|"no match"| R30
  R30 -->|"match"| ACC
  R30 -->|"no match"| R40
  R40 -->|"match"| ACC
  R40 -->|"no match"| R50
  R50 -->|"match"| ACC
  R50 -->|"no match"| DA
  DA --> DROP

Where established/related handling belongs

VyOS 1.4 added a second, arguably better place for the state decision: a global state policy that applies to every base chain at once.

set firewall global-options state-policy established action 'accept'
set firewall global-options state-policy related action 'accept'
set firewall global-options state-policy invalid action 'drop'
set firewall global-options state-policy invalid log

Pick one approach and stay with it. The trade-off is real:

  • Global state-policy — one place, impossible to forget on a new chain, and the return traffic never reaches your rule-sets at all. The cost is that it is global: you cannot decide to treat INVALID differently on one interface.
  • Per-chain rules 10 and 20 — visible in the chain you are reading, tunable per rule-set, and greppable per interface. The cost is that every new chain has to remember to include them, and one that forgets fails quietly in the direction of dropping legitimate return traffic.

Configuring both is the worst option. The global policy accepts the packet before the chain runs, so your rule 10 counter stays at zero and the next engineer concludes the rule is dead and deletes it.

Why established/related is first

Every packet that belongs to an existing flow is admitted without walking the rest of the chain. For a router with a 100-rule chain, the return packets of an established flow are evaluated once rather than a hundred times.

The conntrack state machine determines the state. A packet with a matching entry in the conntrack table is ESTABLISHED or RELATED and is admitted; a packet without one is NEW and the rest of the chain is consulted.

INVALID logging

The INVALID rule is the operator’s window into the chain. Every packet that belongs to no flow and is malformed for its protocol — a TCP segment with ACK set for a connection that was never opened, for instance — is dropped and recorded.

set firewall ipv4 name WAN-IN rule 20 action 'drop'
set firewall ipv4 name WAN-IN rule 20 state 'invalid'
set firewall ipv4 name WAN-IN rule 20 log
set firewall ipv4 name WAN-IN rule 20 log-options level 'info'

log-options level sets the syslog level the rule logs at, which is how you keep firewall noise out of the same bucket as real alerts. Read it back with the log commands that name the chain directly:

show log firewall ipv4 name WAN-IN
show log firewall ipv4 name WAN-IN rule 20

The production use of the INVALID log:

  • Scan detection — a spike in INVALID from a single source is a scanner sending unusual flag combinations.
  • Broken peers — a steady trickle from one specific peer usually means an asymmetric path or a middlebox eating one direction of the handshake, not an attack.
  • Conntrack pressure — when the conntrack table fills, packets for flows that should have been tracked start arriving as INVALID. A rise in INVALID with no new source addresses points at the table, not at the Internet.

Brute-force protection

The recent match tracks source addresses that have recently matched a rule and drops the ones that come back too often.

set firewall ipv4 name WAN-IN rule 25 action 'drop'
set firewall ipv4 name WAN-IN rule 25 description 'SSH brute-force'
set firewall ipv4 name WAN-IN rule 25 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 25 destination port '22'
set firewall ipv4 name WAN-IN rule 25 state 'new'
set firewall ipv4 name WAN-IN rule 25 recent count '5'
set firewall ipv4 name WAN-IN rule 25 recent time 'minute'
set firewall ipv4 name WAN-IN rule 25 log

The rule has to sit before the SSH permit. Rules are evaluated in numeric order and the first match wins, so a brute-force drop numbered after the accept it is supposed to override never runs.

Rate limiting

set firewall ipv4 name WAN-IN rule 40 action 'accept'
set firewall ipv4 name WAN-IN rule 40 description 'HTTPS to web server'
set firewall ipv4 name WAN-IN rule 40 destination address '203.0.113.10'
set firewall ipv4 name WAN-IN rule 40 destination port '443'
set firewall ipv4 name WAN-IN rule 40 protocol 'tcp'
set firewall ipv4 name WAN-IN rule 40 state 'new'
set firewall ipv4 name WAN-IN rule 40 limit rate '50/second'
set firewall ipv4 name WAN-IN rule 40 limit burst '100'

limit rate is written as integer/unit50/second, 5/minute — and limit burst is how far above that rate a short spike may go before the rule stops matching. Note what that means: limit is a condition on the match, not an action. A packet arriving over the rate simply fails to match rule 40 and carries on down the chain, where default-action drop catches it. If you want over-rate traffic handled differently, that is a rule you write, not a behaviour you get.

The anti-patterns

  • Implicit accept on a base chain. A forward filter or input filter with no default-action configured defaults to accept. This is the version of “default accept” that bites people, because nothing in the configuration says it.
  • Missing log. No default-log on the chain and no log on the drop rules. The operator cannot see what is being dropped.
  • Shadowed deny. A drop rule numbered after a more general accept. The drop rule never fires and its counter stays at zero, which is the tell.
  • Permits without state. A rule that admits traffic without state 'new' also admits packets that belong to no flow.
  • Permit any on a sensitive port. SSH, RDP or a database port with no source restriction. Every sensitive port gets a source address or a source interface-group.
  • Both global state-policy and per-chain state rules. The global policy wins, the chain rules never fire, and the zero counters mislead whoever audits next.

Operational commands

# Show the chain and its rules
show firewall ipv4 name WAN-IN

# Show one rule, with its counters
show firewall ipv4 name WAN-IN rule 25

# Show the base chains
show firewall ipv4 forward filter
show firewall ipv4 input filter

# Zone bindings — note the command kept the old name
show firewall zone-policy

# Counters across the whole ruleset
show firewall statistics

# The firewall log, narrowed to this chain
show log firewall ipv4 name WAN-IN

# The rendered nftables ruleset
sudo nft list ruleset

The counters are the part to actually use. A rule whose counter never moves is either dead code or a rule nobody has tested; both are worth knowing about, and neither is visible in the configuration.

Rollback

# See what the candidate would change
compare

# Remove one chain
delete firewall ipv4 name WAN-IN
commit
save

Deleting a rule-set that a base-chain jump-target or a firewall zone ... from ... binding still references fails validation at commit, which is the system doing you a favour. Remove the reference first, then the chain.

rollback N will also take you back to a previous revision, but it reboots the router — on a firewall that is currently carrying traffic, prefer the surgical delete above, and use commit-confirm when the change could cut off your own session.

Production discipline

Cross-course references

  • Part XXXVII-01 (XXXVII-VyOS-Firewall / stateful vs stateless) covers the conntrack layer.
  • Part XXXVII-02 (XXXVII-VyOS-Firewall / zones and chains) covers the zone model.
  • Part XXXVII-03 (XXXVII-VyOS-Firewall / rule ordering) covers the sequence-numbered rules.
  • Part XXXVII-04 (XXXVII-VyOS-Firewall / state tracking) covers connection marks and the recent match.
  • Part XLVIII-03 (XLVIII-VyOS-Logging / firewall logs) covers the log analysis.

Quiz

Knowledge check · 4 questions

  1. Q1. Why is the established/related accept rule placed at the top of a WAN-IN chain?

  2. Q2. On VyOS 1.5 a base chain with no `default-action` configured falls back to accept, while a custom chain with no `default-action` falls back to drop.

  3. Q3. An operator deploys a WAN-IN chain with the canonical rule set but leaves off `default-log` and any `log` on the drop rules. A week later a large share of inbound packets is being dropped and the operator has no evidence about what. What is the failure, and what is the fix?

    The chain has rules 10-50 and `default-action 'drop'`, reached by a `forward filter` jump. Neither `default-log` nor `log` on rule 20 is configured. `show firewall statistics` shows the chain's default action accounting for most of the packets. The operator wants to know which sources are responsible and whether any of them are legitimate.

  4. Q4. An operator adds SSH brute-force protection as rule 50, after the SSH permit at rule 30. The rule never fires. Why, and where should it go?

    WAN-IN has rule 10 (established/related accept), rule 20 (INVALID drop), rule 30 (accept TCP/22 from 198.51.100.0/24, state new), rule 40 (accept HTTPS), and `default-action 'drop'`. The operator adds rule 50: drop, TCP/22, state new, `recent count '5'`, `recent time 'minute'`. `show firewall ipv4 name WAN-IN rule 50` shows a counter of zero after a week.

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