TTR Networks

Intermediate · nftables and iptables on Debian: host and router firewalls · section 3 of 10

A host firewall

✓ Lab-tested 21 October 2026 on Debian 13.7, nftables 1.1.3, iptables 1.8.11

Start with the machine itself: the input hook. The ruleset is a file, loaded as one unit.

The pattern is the same on every server:

  1. Policy drop, so anything not explicitly allowed is refused.
  2. Accept established and related traffic first. It's most of the packets, and it means the machine can still receive replies to anything it starts (updates, DNS, NTP).
  3. Drop invalid packets, and accept loopback: plenty of software talks to itself over 127.0.0.1.
  4. Allow the ICMP that keeps networks working: ping, and the destination unreachable and time exceeded errors that path MTU discovery and traceroute rely on.
  5. Allow each service from where it's needed, and nowhere else: here, SSH from the office LAN only.
  6. A final counter, so you can see how much the policy is dropping.

The output chain keeps policy accept: filtering what a server sends is rarely worth the effort.

nft -c -f checks a file without loading it. systemctl enable --now nftables then loads /etc/nftables.conf now and at every boot. The flush ruleset at the top of the file removes everything first, so loading it twice gives the same result, not duplicate rules.

The result: from the internet, SSH times out (the packets are silently dropped, and the counter shows them), while ping still works. From the office, SSH works.

If the machine has IPv6, the inet table already covers it, but IPv6 needs ICMPv6 for neighbour discovery: add icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, echo-request, packet-too-big, destination-unreachable, time-exceeded } accept, or IPv6 stops working entirely.

/etc/nftables.conf, step 1
#!/usr/sbin/nft -f
# /etc/nftables.conf, step 1: protect the machine itself (the input hook only).
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;

    ct state established,related accept comment "replies to traffic we started"
    ct state invalid drop
    iif lo accept comment "the machine talking to itself"

    icmp type { echo-request, destination-unreachable, time-exceeded } accept
    iifname "eth1" ip saddr 10.10.0.0/24 tcp dport 22 accept comment "SSH from the office LAN only"

    counter comment "everything else hits the policy: drop"
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}
  • Line 3: Start from nothing, so loading the file twice gives the same result.
  • Line 7: Policy drop: anything not allowed below is refused.
  • Lines 9–11: Replies, invalid packets and loopback first. These are the lines you must never leave out.
  • Line 13: The ICMP that keeps the network working.
  • Line 14: SSH only from the office LAN, on the LAN interface.
  • Line 16: Counts what falls through to the policy.
Checking and loading it (real output)
root@fw:~# nft -c -f /etc/nftables.conf && echo "syntax OK"
syntax OK

root@fw:~# systemctl enable --now nftables
Created symlink '/etc/systemd/system/sysinit.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.

root@fw:~# nft list ruleset
table inet filter {
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept comment "replies to traffic we started"
		ct state invalid drop
		iif "lo" accept comment "the machine talking to itself"
		icmp type { destination-unreachable, echo-request, time-exceeded } accept
		iifname "eth1" ip saddr 10.10.0.0/24 tcp dport 22 accept comment "SSH from the office LAN only"
		counter packets 0 bytes 0 comment "everything else hits the policy: drop"
	}

	chain output {
		type filter hook output priority filter; policy accept;
	}
}
  • Lines 1–2: -c checks the file against the kernel without loading anything.
  • Lines 4–5: Load now, and at every boot.
  • Lines 7–22: What's loaded. Note iif "lo", the reordered ICMP set, and the counter at zero.
Testing it (real output)
root@inet:~# nc -zv -w 2 203.0.113.10 22; ping -c 1 -W 1 203.0.113.10 | tail -2
nc: connect to 203.0.113.10 port 22 (tcp) timed out: Operation now in progress
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.039/0.039/0.039/0.000 ms

root@pc:~# ssh fw hostname
fw

root@fw:~# nft list chain inet filter input | grep -E "SSH|everything"
		iifname "eth1" ip saddr 10.10.0.0/24 tcp dport 22 accept comment "SSH from the office LAN only"
		counter packets 2 bytes 120 comment "everything else hits the policy: drop"
  • Lines 1–4: From the internet: SSH times out, ping still works.
  • Lines 6–7: From the office: SSH works.
  • Lines 10–11: Two packets dropped: the internet host's SYN and its retry.

Check your understanding

Why does the input chain accept ct state established,related before anything else?

  • So replies to connections the machine started, and packets of connections already allowed, get through without rules of their own.
  • Because nftables requires it as the first rule.
  • To allow all traffic from the LAN.
  • It speeds up the ICMP rule.