Knowledge base

A sensible nftables baseline

A default-drop ruleset that permits what an instance genuinely needs, rate-limits SSH, and does not lock you out while you are still writing it.

One firewall, not two

Debian 13 ships nftables with an /etc/nftables.conf and a service that loads it. AlmaLinux 10 ships firewalld on top of nftables, and its service reads what /etc/sysconfig/nftables.conf includes. Pick one. Running firewalld alongside a hand-written ruleset produces a ruleset neither of you wrote.

systemctl disable --now firewalld     # AlmaLinux, if going manual
systemctl enable --now nftables

The ruleset

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  set ssh_flood {
    type ipv4_addr
    flags dynamic, timeout
    timeout 1h
  }

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

    ct state established,related accept
    ct state invalid drop
    iif lo accept

    ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
    ip6 nexthdr ipv6-icmp accept

    tcp dport 22 ct state new add @ssh_flood { ip saddr limit rate over 6/minute } drop
    tcp dport 22 accept
    tcp dport { 80, 443 } accept
    udp dport 443 accept

    counter comment "dropped by policy"
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

The parts people get wrong:

  • ICMPv6 is accepted wholesale on purpose. Neighbour discovery and packet-too-big both live there, and filtering them by hand breaks IPv6 in ways that present as an application bug for a day and a half.
  • The ssh_flood set adds any address exceeding six new connections a minute to a one-hour timeout list. No log parser, no cron job, no second daemon.
  • For IPv6, duplicate the set with type ipv6_addr and the rule with ip6 saddr. Sets are single-family.
  • udp dport 443 is there for QUIC. Delete it if you do not serve it.

Load it without locking yourself out

systemd-run --on-active=120 nft flush ruleset
nft -f /etc/nftables.conf

The first command schedules a transient unit that clears the ruleset in two minutes and prints its name. Open a fresh session. If it works, stop the timer:

systemctl stop run-u1234.timer

If it does not work, wait, and the machine lets you back in by itself. An empty ruleset means no filtering, so finish the job rather than leaving it there overnight.

Check what it is actually doing

nft list ruleset
nft -a list chain inet filter input
nft list set inet filter ssh_flood

-a prints handles, which is how a single rule is removed without rewriting the file:

nft delete rule inet filter input handle 12

Counters answer the question nobody asks: a rule sitting at zero after a week is either wrong or unnecessary, and both are worth knowing.

Tracing

When a packet refuses to behave the way the ruleset says it should:

nft add rule inet filter input tcp dport 8443 meta nftrace set 1
nft monitor trace

Delete the trace rule afterwards. It is cheap rather than free.

What a host firewall is not for

It filters policy, not floods. Volumetric attacks are dropped at the edge long before they reach your port, and a rule on the instance would only be spending your CPU on packets that were already handled. Details on DDoS protection.

Persist it

nft list ruleset > /etc/nftables.conf
systemctl restart nftables

Put the shebang and the flush ruleset line back at the top of the exported file. Without them the next load appends to the running ruleset instead of replacing it, and every rule you have appears twice.

Ready when you are

Pick a city. Pick a size. Pay in coin.

No forms about who you are, no wait for a human to approve you, no phone call to verify anything. The invoice clears and the credentials land in your inbox.