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.
| Segment | Prefix | fw | srv | net |
|---|---|---|---|---|
| LAB-INSIDE | 192.0.2.0/24 | eth1 · .1 | eth1 · .10 | — |
| LAB-OUTSIDE | 203.0.113.0/30 | eth2 · .1 | — | eth1 · .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 (
fwneeds 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 thezone-policytree. 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 adefault-action dropto the chain that carries your SSH session to the router. It is written to be safe, it usescommit-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 thecommitin Task 6. - Two traffic generators, and nothing more exotic:
pingfor ICMP and an outboundsshfor 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:
$ show firewall statisticsRulesets Information
No configured rulesetsIllustrative 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.10reaches a password prompt. Rule 30.ping 192.0.2.10 count 3gets no reply. There is no ICMP permit, so it falls off the end of the chain todefault-action drop.
Read both halves of the evidence. First the counters:
$ show firewall ipv4 name WAN-TO-LANRuleset 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 252Illustrative output
Then the log, which is the half that says what was dropped rather than how much:
$ show log firewall ipv4 name WAN-TO-LANAug 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=1Illustrative 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:
$ show conntrack table ipv4Find 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
fwstill responds. Rule 10. - From
srv,ping 192.0.2.1 count 3answers. Rule 20. - From
net,ping 203.0.113.1 count 3gets nothing andssh vyos@203.0.113.1times out. The default action, andshow log firewall ipv4 input filternames 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 filterandshow firewall ipv4 input filterboth show an explicitdefault-actionofdrop.show firewall ipv4 name WAN-TO-LANshowsdefault-action dropwithdefault-log, an established/related accept at rule 10, an INVALID drop withlogat 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.10reaches a password prompt and rule 30’s counter advances by the number of attempts;ping 192.0.2.10 count 3gets no reply and thedefaultcounter advances by three. - From
net:ping 203.0.113.1 count 3andssh vyos@203.0.113.1both fail, andshow log firewall ipv4 input filtercarries the dropped packets. - From
srv:ping 203.0.113.2 count 3andssh vyos@203.0.113.2both succeed, which proves both directions of policy are present. - Your management session to
fwsurvived the Task 6commit-confirmand you ranconfirmonly after all four checks passed. - The journal holds a conntrack capture taken while an outbound connection was hanging, showing the flow in
SYN_SENTwith 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-policylists two zones and twofrombindings, 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
eth2goes toinput filterif it is addressed to the router and toforward filterif 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-actionis 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_SENTwaiting 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 WANandzone WAN from LANare 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.