Skip to main content
RunBook Academy

← All labs in VyOS

Lab · advanced · ~110 min

Lab: VyOS Stateful Firewall

B · Nested virtualisationC · Simulation

Objectives

  • Demonstrate that the destination, not the ingress interface, decides which base chain sees a packet
  • Show that a base chain with no `default-action` accepts while a custom chain with none drops, and say why that asymmetry is dangerous
  • Build a default-deny rule-set and reach it with a `forward filter` jump carrying both interface directions
  • Break outbound connectivity by omitting the return direction, and diagnose it from conntrack rather than from the rules
  • Reproduce a shadowed drop rule, identify it from a zero counter, and fix it by renumbering
  • Close the router control plane with an explicit `input filter` default-action without losing management access
  • Express the same policy as zones, read the from-direction model correctly, and meet the one-directional trap

Prerequisites

Objective

By the end of this lab the router will drop everything from the outside except one explicitly permitted flow, admit return traffic for anything the inside started, and refuse connections to its own control plane from anywhere but the management interface — and every one of those claims will be backed by a counter that moved and a log line that names the rule.

The reason to do it in that order is that a VyOS firewall has three independent ways to be wrong while looking right. A rule-set nothing jumps to is invisible. A base chain with no default-action accepts everything and says nothing about it. A drop rule numbered after the accept it was meant to override never runs and leaves a counter at zero. None of the three produce an error, and all three are found the same way: by generating one packet and reading what moved.

Architecture

One firewall with an inside and an outside, and one host on each side so that every direction has something to send.

                  192.0.2.10/24
                    +--------+
                    |  srv   |   internal server
                    |  eth1  |   sshd on 22
                    +---+----+
                        |
              bridge LAB-INSIDE  (no uplink)
                        |
                   eth1 | 192.0.2.1/24
                +-------+--------+
                |       fw       |
                |     eth2       |
                +-------+--------+
                        | 203.0.113.1/30
              bridge LAB-OUTSIDE  (no uplink)
                        | 203.0.113.2/30
                    +---+----+
                    |  net   |   stands in for the Internet
                    |  eth1  |   sshd on 22
                    +--------+

   eth0 on all three VMs: your management LAN.
   Task 6 is the only task that touches how it is treated,
   and it does so from the console.

Both srv and net run VyOS, and both run sshd. That is a deliberate, and slightly unusual, choice: the lab needs a TCP service on each side that a plain VyOS install is guaranteed to answer, and SSH is the only one. A production WAN-TO-LAN chain would carry HTTPS and SMTP permits instead — the rule shape is identical and the port number is the only thing that changes. ICMP does the rest of the work, because it is the one thing every host answers and the one thing that is easy to send on demand.

SegmentPrefixfwsrvnet
LAB-INSIDE192.0.2.0/24eth1 · .1eth1 · .10
LAB-OUTSIDE203.0.113.0/30eth2 · .1eth1 · .2

Addresses come from the ranges RFC 5737 reserves for documentation, and both bridges are isolated, so nothing here can reach a real network even if a bridge is misconfigured.

Requirements

  • A hypervisor that can run three VMs, each with one NIC on your management LAN and one on an isolated bridge (fw needs two). The build block is written for Proxmox VE; any KVM/libvirt host works.
  • 2 GB RAM and 8 GB disk per VM — 6 GB and 24 GB in total.
  • VyOS 1.5 LTS. This matters throughout. The per-interface firewall binding (set interfaces ethernet eth0 firewall in name WAN-IN) was removed in 1.4, and so was the zone-policy tree. A 1.3-era runbook fails at commit here, which is the good case; the bad case is deleting the failing line and believing the remaining rule-set does something.
  • Console access to fw — not optional. Task 6 applies a default-action drop to the chain that carries your SSH session to the router. It is written to be safe, it uses commit-confirm, and you should still have the console open before you start it. Name the step where you would lose the session if you got it wrong: it is the commit in Task 6.
  • Two traffic generators, and nothing more exotic: ping for ICMP and an outbound ssh for TCP. If your image’s operational shell does not offer one of them, substitute anything that opens a TCP connection to port 22 — the tests in this lab are written so that the rule counters and the firewall log are the authoritative evidence, and the client is only the trigger.
  • Roughly 110 minutes, of which the VM build is about 30.

Scenario

The router in front of a small internal network has been forwarding happily and filtering nothing. The change request is one line — “publish SSH to the internal server, block everything else inbound” — and it arrives with an implicit second half that nobody wrote down: outbound traffic and management access must keep working while you do it.

That second half is where firewalls cause outages. The inbound policy is the part everybody reviews. The return direction, the control plane and the rule ordering are the parts that break, and they break in ways that look like an application fault rather than a firewall change.

Tasks

Task 1: Build the starting state and prove it is open

Two isolated bridges on the hypervisor. This is configuration text for /etc/network/interfaces, not a command:

auto vmbr96
iface vmbr96 inet manual
    bridge-ports none
    bridge-stp off
    bridge-fd 0

auto vmbr97
iface vmbr97 inet manual
    bridge-ports none
    bridge-stp off
    bridge-fd 0

Apply with ifreload -a, then build the VMs:

# Run on the Proxmox host.
# Substitute your own values. The ISO volume ID must match what
# `pvesm list local` prints for your upload.
ISO=local:iso/vyos-1.5-lts-amd64.iso
STORE=local-lvm

qm create 217 --name fw --memory 2048 --cores 2 \
  --net0 "virtio,bridge=vmbr0,firewall=0" \
  --net1 "virtio,bridge=vmbr96,firewall=0" \
  --net2 "virtio,bridge=vmbr97,firewall=0" \
  --scsihw virtio-scsi-single --scsi0 "$STORE:8" \
  --ide2 "$ISO,media=cdrom" --boot order=ide2 --ostype l26

qm create 218 --name srv --memory 2048 --cores 2 \
  --net0 "virtio,bridge=vmbr0,firewall=0" \
  --net1 "virtio,bridge=vmbr96,firewall=0" \
  --scsihw virtio-scsi-single --scsi0 "$STORE:8" \
  --ide2 "$ISO,media=cdrom" --boot order=ide2 --ostype l26

qm create 219 --name net --memory 2048 --cores 2 \
  --net0 "virtio,bridge=vmbr0,firewall=0" \
  --net1 "virtio,bridge=vmbr97,firewall=0" \
  --scsihw virtio-scsi-single --scsi0 "$STORE:8" \
  --ide2 "$ISO,media=cdrom" --boot order=ide2 --ostype l26

for VMID in 217 218 219; do qm start "$VMID"; done

firewall=0 on every NIC is doing real work here. Proxmox’s per-interface firewall runs on the host; leaving it enabled while you debug a guest firewall gives you two filters in series and only one of them in show firewall statistics.

Install VyOS on each from the console with install image, then point the boot order back at the disk and detach the ISO. Configure eth0 for your management LAN in whatever way your environment expects, then paste the rest.

On fw:

configure
set system host-name fw
set interfaces ethernet eth1 address '192.0.2.1/24'
set interfaces ethernet eth1 description 'inside'
set interfaces ethernet eth2 address '203.0.113.1/30'
set interfaces ethernet eth2 description 'outside'
set service ssh
commit
save /config/pre-lab-15.boot
save

On srv:

configure
set system host-name srv
set interfaces ethernet eth1 address '192.0.2.10/24'
set protocols static route 0.0.0.0/0 next-hop '192.0.2.1'
set service ssh
commit
save

On net:

configure
set system host-name net
set interfaces ethernet eth1 address '203.0.113.2/30'
set protocols static route 192.0.2.0/24 next-hop '203.0.113.1'
set service ssh
commit
save

Now establish that nothing is filtered, because “it stopped working” is only evidence if you know it worked. From net:

ping 192.0.2.10 count 3
ssh vyos@192.0.2.10

Both succeed — the ping answers and SSH gets as far as a password prompt. Exit the SSH session. From srv, the same two toward 203.0.113.2. And on fw:

Read-only / Safefw
$ show firewall statistics
Rulesets Information

No configured rulesets

Illustrative output

Write all four results into the journal. Every later test is a comparison against this row.

Task 2: Find out which chain sees which packet

Filtering happens in three base chains per address family, and which one sees a packet is decided by where the packet is going, not by the interface it arrived on. That sentence is the single most expensive misconception in this part of the configuration, so prove it rather than accepting it.

Add two rules that do nothing but count. Both accept, so nothing is filtered and nothing can break:

configure
set firewall ipv4 input filter rule 5 action 'accept'
set firewall ipv4 input filter rule 5 description 'COUNTER ONLY - traffic addressed to this router from outside'
set firewall ipv4 input filter rule 5 inbound-interface name 'eth2'

set firewall ipv4 forward filter rule 5 action 'accept'
set firewall ipv4 forward filter rule 5 description 'COUNTER ONLY - traffic routed in from outside'
set firewall ipv4 forward filter rule 5 inbound-interface name 'eth2'
commit

Both rules match on the same interface. Read both counters, then send two pings from net — one to the router’s own outside address, one through the router to srv — then read both counters again:

show firewall ipv4 input filter
show firewall ipv4 forward filter
ping 203.0.113.1 count 3
ping 192.0.2.10 count 3

The first ping moves the input filter counter. The second moves the forward filter counter. Same ingress interface, same source, different chain — because the first packet was addressed to the router and the second was not.

Leave the two counter rules in place for now — they will keep telling you which chain a test landed in — and remove them in Task 6 when the input filter gets a real policy.

Task 3: Build the inbound policy and reach it

A named rule-set is a chain with no packets in it. It exists, it holds rules and a default-action, and nothing walks it until a base chain says action jump and names it.

configure
set firewall ipv4 name WAN-TO-LAN description 'inbound from the outside towards the inside'
set firewall ipv4 name WAN-TO-LAN default-action 'drop'
set firewall ipv4 name WAN-TO-LAN default-log

set firewall ipv4 name WAN-TO-LAN rule 10 action 'accept'
set firewall ipv4 name WAN-TO-LAN rule 10 description 'return traffic for flows the inside started'
set firewall ipv4 name WAN-TO-LAN rule 10 state 'established'
set firewall ipv4 name WAN-TO-LAN rule 10 state 'related'

set firewall ipv4 name WAN-TO-LAN rule 20 action 'drop'
set firewall ipv4 name WAN-TO-LAN rule 20 description 'invalid - logged separately to surface scanners'
set firewall ipv4 name WAN-TO-LAN rule 20 state 'invalid'
set firewall ipv4 name WAN-TO-LAN rule 20 log
set firewall ipv4 name WAN-TO-LAN rule 20 log-options level 'info'

set firewall ipv4 name WAN-TO-LAN rule 30 action 'accept'
set firewall ipv4 name WAN-TO-LAN rule 30 description 'published SSH to the internal server'
set firewall ipv4 name WAN-TO-LAN rule 30 protocol 'tcp'
set firewall ipv4 name WAN-TO-LAN rule 30 source address '203.0.113.2/32'
set firewall ipv4 name WAN-TO-LAN rule 30 destination address '192.0.2.10'
set firewall ipv4 name WAN-TO-LAN rule 30 destination port '22'
set firewall ipv4 name WAN-TO-LAN rule 30 state 'new'
commit
save

Three details in that block are worth naming. state 'established' and state 'related' are bare values on a multi-valued leaf — there is no enable beneath state on 1.4 or later. log is a bare node here, unlike the policy route tree where it takes an explicit enable; they are different trees with different conventions and the completion tells you which one you are in. And there is no rule 9999 “catch-all deny for documentation”: default-action drop already does it and default-log already logs it, so a duplicate explicit rule would give you a second counter that disagrees with the first.

Now commit that on its own and test from net. Nothing changes: the ping still works, SSH still works. The rule-set is complete and it is not in the path of a single packet.

Reach it:

configure
set firewall ipv4 forward filter default-action 'drop'
set firewall ipv4 forward filter rule 10 action 'jump'
set firewall ipv4 forward filter rule 10 jump-target 'WAN-TO-LAN'
set firewall ipv4 forward filter rule 10 inbound-interface name 'eth2'
set firewall ipv4 forward filter rule 10 outbound-interface name 'eth1'
commit

The jump rule names both interfaces, and the pair is what makes it a direction. A jump that names only inbound-interface sends everything arriving on that interface into the rule-set regardless of where it is going, which is nearly always more traffic than the rule-set was written for.

Test from net again:

  • ssh vyos@192.0.2.10 reaches a password prompt. Rule 30.
  • ping 192.0.2.10 count 3 gets no reply. There is no ICMP permit, so it falls off the end of the chain to default-action drop.

Read both halves of the evidence. First the counters:

Read-only / Safefw
$ show firewall ipv4 name WAN-TO-LAN
Ruleset Information

ipv4 Firewall "name WAN-TO-LAN"

Rule     Action    Protocol    Packets    Bytes    Conditions
-------  --------  ----------  ---------  -------  ------------------------------
10       accept    all         218        19K      state established,related
20       drop      all         0          0        state invalid
30       accept    tcp         3          180      saddr 203.0.113.2 daddr 192.0.2.10 dport 22
default  drop                  3          252

Illustrative output

Then the log, which is the half that says what was dropped rather than how much:

Read-only / Safefw
$ show log firewall ipv4 name WAN-TO-LAN
Aug 19 14:23:45 fw kernel: [ruleset-rule-action] IN=eth2 OUT=eth1 SRC=203.0.113.2 DST=192.0.2.10 PROTO=ICMP TYPE=8 CODE=0 ID=1842 SEQ=1

Illustrative output

The fields after the bracketed prefix are netfilter’s and they are stable: IN= and OUT= are the ingress and egress interfaces, SRC/DST the addresses, and the protocol block whatever the protocol carries. The prefix itself is composed by VyOS from the address family, the rule-set, the rule number and the action, and the exact composition has changed between releases — so learn its shape from a rule you made fire on your own box rather than from this page. Once you have it, the prefix is what turns “something dropped this” into “the default action of WAN-TO-LAN dropped this”.

Now change one thing to prove the source restriction is real. Point rule 30 at a source net does not have:

configure
set firewall ipv4 name WAN-TO-LAN rule 30 source address '198.51.100.0/24'
commit

ssh vyos@192.0.2.10 from net now hangs and eventually times out rather than being refused, and rule 30’s counter stops moving while the default counter climbs. That difference is worth internalising: a timeout means something dropped the packet silently, a connection refused means the packet arrived and nothing was listening. They point at completely different halves of the stack.

Put the source back to 203.0.113.2/32 and confirm SSH works again.

Task 4: Forget the return direction, and diagnose it from state

Right now forward filter has default-action drop and exactly one jump rule, for traffic arriving on eth2. Traffic from the inside going out has no jump rule at all, so it hits the base chain’s default action.

From srv:

ping 203.0.113.2 count 3

It fails. Add the outbound policy:

configure
set firewall ipv4 name LAN-TO-WAN description 'outbound from the inside'
set firewall ipv4 name LAN-TO-WAN default-action 'drop'
set firewall ipv4 name LAN-TO-WAN default-log
set firewall ipv4 name LAN-TO-WAN rule 20 action 'accept'
set firewall ipv4 name LAN-TO-WAN rule 20 description 'inside networks may start anything outbound'
set firewall ipv4 name LAN-TO-WAN rule 20 source address '192.0.2.0/24'
set firewall ipv4 name LAN-TO-WAN rule 20 state 'new'

set firewall ipv4 forward filter rule 20 action 'jump'
set firewall ipv4 forward filter rule 20 jump-target 'LAN-TO-WAN'
set firewall ipv4 forward filter rule 20 inbound-interface name 'eth1'
set firewall ipv4 forward filter rule 20 outbound-interface name 'eth2'
commit

Note what is deliberately missing: LAN-TO-WAN has no established/related accept. Test from srv again:

ping 203.0.113.2 count 3
ssh vyos@203.0.113.2

The ping now gets replies — because the reply is traffic arriving on eth2 and leaving on eth1, which lands in WAN-TO-LAN, whose rule 10 accepts established and related. Good. Now start a flow the other way and watch the difference, by removing rule 10 from WAN-TO-LAN:

configure
delete firewall ipv4 name WAN-TO-LAN rule 10
commit

From srv, ssh vyos@203.0.113.2 now hangs. It does not fail fast; it sits there. Every outbound connection from every host behind this firewall behaves the same way, immediately after a firewall change, and the reports that arrive will be about the application.

Diagnose it from state rather than from the rules:

Read-only / Safefw
$ show conntrack table ipv4

Find the row for the flow you just started — source 192.0.2.10, destination 203.0.113.2, destination port 22. It is there, and its TCP state is SYN_SENT. Conntrack saw the outbound SYN, created the entry, and is waiting for a reply it will never be shown, because no rule-set on the return path consults state any more. That is the whole diagnosis, and it took one command. show conntrack statistics answers the adjacent question — whether the table itself is under pressure — which is the other reason packets start arriving judged INVALID.

Confirm it from the other side too:

show firewall ipv4 name WAN-TO-LAN

The default counter is climbing while every named rule stays still. Then put rule 10 back:

configure
set firewall ipv4 name WAN-TO-LAN rule 10 action 'accept'
set firewall ipv4 name WAN-TO-LAN rule 10 description 'return traffic for flows the inside started'
set firewall ipv4 name WAN-TO-LAN rule 10 state 'established'
set firewall ipv4 name WAN-TO-LAN rule 10 state 'related'
commit
save

Task 5: Build a shadowed rule, then find it from its counter

Add brute-force protection for the published SSH service, and add it in the wrong place on purpose — after the permit it is meant to override:

configure
set firewall ipv4 name WAN-TO-LAN rule 50 action 'drop'
set firewall ipv4 name WAN-TO-LAN rule 50 description 'SSH brute-force'
set firewall ipv4 name WAN-TO-LAN rule 50 protocol 'tcp'
set firewall ipv4 name WAN-TO-LAN rule 50 destination port '22'
set firewall ipv4 name WAN-TO-LAN rule 50 state 'new'
set firewall ipv4 name WAN-TO-LAN rule 50 recent count '5'
set firewall ipv4 name WAN-TO-LAN rule 50 recent time 'minute'
set firewall ipv4 name WAN-TO-LAN rule 50 log
commit

recent time takes a unit, not a number of seconds: second, minute or hour. Together with recent count '5' the rule reads “five or more matches from this source within the last minute”. The 1.3 form recent time 60 does not commit, and the shape changed with it — there is no separate rule to record hits any more, one rule both counts and acts.

From net, open and close SSH to 192.0.2.10 six or seven times in quick succession. Then:

show firewall ipv4 name WAN-TO-LAN rule 50
show firewall ipv4 name WAN-TO-LAN rule 30

Rule 30’s counter has climbed by the number of attempts. Rule 50’s is still zero, and it will stay zero forever. Rules are evaluated in numeric order and the first match wins, so for a source inside 203.0.113.2/32 rule 30 accepts every SYN before rule 50 is reached — and for any other source, default-action drop catches the packet, again before rule 50 has any effect. The rule is dead code for every possible source, which is exactly what the zero counter is telling you.

Fix it by renumbering ahead of the permit:

configure
delete firewall ipv4 name WAN-TO-LAN rule 50
set firewall ipv4 name WAN-TO-LAN rule 25 action 'drop'
set firewall ipv4 name WAN-TO-LAN rule 25 description 'SSH brute-force'
set firewall ipv4 name WAN-TO-LAN rule 25 protocol 'tcp'
set firewall ipv4 name WAN-TO-LAN rule 25 destination port '22'
set firewall ipv4 name WAN-TO-LAN rule 25 state 'new'
set firewall ipv4 name WAN-TO-LAN rule 25 recent count '5'
set firewall ipv4 name WAN-TO-LAN rule 25 recent time 'minute'
set firewall ipv4 name WAN-TO-LAN rule 25 log
commit
save

Repeat the burst of SSH attempts and watch rule 25’s counter move and the connections start failing partway through the burst. Verify by watching the counter, not by assuming the renumber worked.

Then wait a minute before the next test. The rule has no source exception in it, so your own address is now over the threshold and the published service will refuse you until the window rolls. That is the protection working, and it is also the reason a production deployment puts the operator’s jump host in a source group that the brute-force rule skips — a control you get to design once and are grateful for at two in the morning.

Task 6: Close the control plane without locking yourself out

Everything so far has been about traffic through the router. The input filter chain is traffic to it — your SSH session, the API, any routing protocol — and it is currently accepting everything because no default-action has been set on it.

configure
delete firewall ipv4 input filter rule 5

set firewall ipv4 input filter rule 5 action 'accept'
set firewall ipv4 input filter rule 5 description 'return traffic for sessions this router started'
set firewall ipv4 input filter rule 5 state 'established'
set firewall ipv4 input filter rule 5 state 'related'

set firewall ipv4 input filter rule 10 action 'accept'
set firewall ipv4 input filter rule 10 description 'management plane - everything arriving on the management interface'
set firewall ipv4 input filter rule 10 inbound-interface name 'eth0'

set firewall ipv4 input filter rule 20 action 'accept'
set firewall ipv4 input filter rule 20 description 'ICMP to the router from the inside, for diagnosis'
set firewall ipv4 input filter rule 20 protocol 'icmp'
set firewall ipv4 input filter rule 20 inbound-interface name 'eth1'

set firewall ipv4 input filter default-action 'drop'
set firewall ipv4 input filter default-log
commit-confirm 5

Your session should still be alive. Prove the policy from both sides before you confirm — that is what the five minutes are for:

  • From your management host, the SSH session to fw still responds. Rule 10.
  • From srv, ping 192.0.2.1 count 3 answers. Rule 20.
  • From net, ping 203.0.113.1 count 3 gets nothing and ssh vyos@203.0.113.1 times out. The default action, and show log firewall ipv4 input filter names it.

Only when all four hold:

confirm
save

If any of them failed, do nothing at all. In five minutes the router reverts to the configuration it had before, on its own, and you can read the candidate again with a session that still exists.

Remove the remaining counter rule from Task 2 now that forward filter has real policy: delete firewall ipv4 forward filter rule 5, commit, save.

Task 7: The same policy as zones, and the trap in it

Zones are an optional layer that groups interfaces into trust domains and lets you write policy in terms of the groups. The base-chain and jump model you have built is a complete firewall on its own; zones earn their keep when a trust domain spans several interfaces, or when the number of interface pairs has outgrown what a person can read.

Build the zone form. The base chain goes first — the zone layer generates its own chains and its own dispatch rules into the same hooks, and leaving hand-written rules there means the combined order is the generator’s decision rather than yours:

configure
delete firewall ipv4 forward filter

set firewall zone LAN description 'inside'
set firewall zone LAN interface 'eth1'
set firewall zone LAN default-action 'drop'

set firewall zone WAN description 'outside'
set firewall zone WAN interface 'eth2'
set firewall zone WAN default-action 'drop'

set firewall zone LAN from WAN firewall name 'WAN-TO-LAN'
set firewall zone WAN from LAN firewall name 'LAN-TO-WAN'
commit-confirm 5

Read the binding right to left. set firewall zone LAN from WAN firewall name 'WAN-TO-LAN' attaches the rule-set to the destination zone and names the source zone with from: “traffic arriving into LAN, coming from WAN, is filtered by WAN-TO-LAN”. There is no to zone form on 1.5 and there never was one in the zone-policy tree it replaced.

Deleting the whole forward filter node takes its default-action with it, and that is intentional. Each zone carries its own default-action, and between four zones there are twelve possible directions of which this configuration writes two — the other ten are governed by the destination zone’s default. That value is policy, not a formality.

Two things are deliberately absent. There is no local-zone, because Task 6 already gave the control plane an explicit input filter policy and defining both would be two policies competing for the same traffic. And eth0 is in no zone, because no traffic transits it in this topology — its only role is management, which is input-chain traffic.

Verify, then confirm:

show firewall zone-policy

The operational command kept the old name even though the configuration tree moved from zone-policy zone to firewall zone in 1.4, which is worth remembering the first time a completion refuses what you typed. Re-run the Task 3 and Task 4 tests: SSH from net to 192.0.2.10 works, ping from net to 192.0.2.10 is dropped, ping 203.0.113.2 count 3 from srv works. Then confirm and save.

Now the trap. Delete one direction:

configure
delete firewall zone WAN from LAN
commit

From srv, ping 203.0.113.2 count 3 fails and ssh vyos@203.0.113.2 hangs. zone WAN from LAN and zone LAN from WAN are two unrelated pieces of configuration; deleting the first leaves outbound traffic handled by the WAN zone’s default-action drop, and no rule-set on that path has anything to say about it. Every zone pair you care about needs both directions written, and each direction needs its own established and related accept near the top of its rule-set.

Restore it, commit, save, and re-test.

Validation

  • show firewall ipv4 forward filter and show firewall ipv4 input filter both show an explicit default-action of drop.
  • show firewall ipv4 name WAN-TO-LAN shows default-action drop with default-log, an established/related accept at rule 10, an INVALID drop with log at rule 20, the brute-force drop at rule 25, and the SSH permit at rule 30 — in that numeric order.
  • From net: ssh vyos@192.0.2.10 reaches a password prompt and rule 30’s counter advances by the number of attempts; ping 192.0.2.10 count 3 gets no reply and the default counter advances by three.
  • From net: ping 203.0.113.1 count 3 and ssh vyos@203.0.113.1 both fail, and show log firewall ipv4 input filter carries the dropped packets.
  • From srv: ping 203.0.113.2 count 3 and ssh vyos@203.0.113.2 both succeed, which proves both directions of policy are present.
  • Your management session to fw survived the Task 6 commit-confirm and you ran confirm only after all four checks passed.
  • The journal holds a conntrack capture taken while an outbound connection was hanging, showing the flow in SYN_SENT with no reply.
  • The journal holds rule 50’s zero counter next to rule 30’s advancing one, and rule 25’s advancing counter after the renumber.
  • show firewall zone-policy lists two zones and two from bindings, and your notes say which model you chose to keep and why.

Expected Outcome

A router that drops inbound traffic by default, publishes exactly one service to exactly one source, admits return traffic for anything the inside started, logs everything it drops, and answers on its own control plane only from the management interface and from the inside for ICMP.

More importantly, a set of habits: every chain carries an explicit default-action; every drop rule and every chain default carries a log; every claim about the policy is backed by a counter delta rather than by the configuration; and every change that could reach the management path goes in under commit-confirm with the console open.

The topology is self-contained. Nothing later in the course depends on it, so destroy it whenever you are finished.

Troubleshooting

The rule-set exists and nothing is filtered. Nothing jumps to it. Check show firewall ipv4 forward filter for an action jump rule whose jump-target names your rule-set, and check that the jump rule’s own counter is moving. A named rule-set with no caller is invisible and produces no error.

Everything from the outside is dropped, including the service you published. Read the counters top down. The first rule whose counter is moving when it should not be is your answer — usually rule 20 catching traffic conntrack has judged INVALID, or a permit whose destination address does not match where the traffic is actually going.

Outbound connections hang instead of failing. The return direction. Check that the rule-set on the inbound path has an established/related accept, or that global-options state-policy is providing one — and check that you are not doing both, which leaves your rule counter at zero and misleads whoever reads it next.

A drop rule’s counter never moves. Either it is shadowed by an accept above it, or it is unreachable because the chain default catches the traffic first. Both are dead code and both look identical in the configuration. Read the rule numbers, then read the counters.

IPv4 is closed and something still gets in. IPv6. firewall ipv6 input filter and firewall ipv6 name ... are separate chains with separate rules, and nothing you wrote for IPv4 applies to them.

Everything looks right and behaviour disagrees. Read what was actually built with sudo nft list ruleset. This is especially worth doing if zones and hand-written base-chain rules are both present, since the combined order is the generator’s decision.

You have lost your session to fw. Use the console. If the change went in under commit-confirm, wait — the router reverts on its own and the session comes back. If it did not, load /config/pre-lab-15.boot from the console, compare, commit.

Cleanup

Everything here runs on isolated bridges, so cleanup means returning to a known state rather than restoring service. Work down until you reach the state you want.

Step 1. To keep the firewall for further practice — the firewall and NAT break/fix scenarios use this shape — stop here and save on all three routers. Note in the journal that fw is running a default-deny input filter, so the next person is not surprised when a new management source cannot reach it.

Step 2. To remove the policy but keep the topology, take the references out before the rule-sets. On fw:

configure
delete firewall zone LAN
delete firewall zone WAN
delete firewall ipv4 input filter
delete firewall ipv4 forward filter
delete firewall ipv4 name WAN-TO-LAN
delete firewall ipv4 name LAN-TO-WAN
commit
save

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

If you stopped before Task 7, the two firewall zone lines have nothing to delete and VyOS says so rather than failing the commit; if you completed Task 7, the same is true of firewall ipv4 forward filter. Either way the block is safe to paste whole.

Confirm the router is open again from both sides — ping and ssh in each direction — because “I deleted the firewall” and “traffic flows” are two different claims and only the second one matters.

Step 3. To reset fw completely, load the file saved in Task 1:

configure
load /config/pre-lab-15.boot
compare
commit
save

Read the compare output before committing. It is the only thing between you and a stored revision that turns out not to be the one you meant.

Step 4. To remove the topology entirely, stop and destroy the three VMs, then delete vmbr96 and vmbr97 from /etc/network/interfaces and run ifreload -a.

What You Learned

  • The destination picks the chain, not the interface. A packet arriving on eth2 goes to input filter if it is addressed to the router and to forward filter if it is being routed onward. An SSH rule written in the wrong one of those never matches anything, and nothing says so.
  • The defaults run the wrong way for safety. Base chains accept when no default-action is configured; custom chains drop. A half-built firewall therefore fails open exactly where it faces the network. Write the default explicitly, on every chain, including the IPv6 ones.
  • A rule-set with no caller is invisible. You committed a complete default-deny policy and watched it filter nothing at all, because nothing jumped to it. The jump rule’s own counter is the fastest way to tell that case apart from a rule-set that is being reached and is not matching.
  • State handling belongs in exactly one place. Removing the established accept from one direction broke every outbound connection from every host, and presented as an application hang rather than as a firewall error. Conntrack found it in one command, because the flow was sitting there in SYN_SENT waiting for a reply nobody would admit.
  • A zero counter is a finding. Rule 50 was valid configuration, reviewed clean, and could never fire. The configuration cannot tell you that; only the counter can, which is why the deliverable for this lab is a set of before-and-after readings rather than a rule-set.
  • The control plane is a separate policy from the transit policy. It has its own chain, its own default action, and its own way of going wrong — and closing it is the one change in this lab that can end your session, which is why it goes in with the console open and a timer running.
  • Zones are one-directional, and both directions are yours to write. zone LAN from WAN and zone WAN from LAN are unrelated configuration. Writing one of them produces a firewall where outbound connections hang, which is the same symptom as the missing established accept and a different cause.

Deliverables

  • · A rule-by-rule counter reading taken before and after each traffic test, with the deltas written out
  • · The firewall log extract that identifies which rule dropped a specific packet
  • · A conntrack capture taken while an outbound connection was hanging, showing the flow present and the reply dropped
  • · A written control-plane policy: which sources may reach the router, on which interface, and what proves it

Verification status

Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.