I migrated off ufw to nftables because I wanted to read the actual ruleset and tweak it without a wrapper guessing what I meant. One stateful set covers almost everything a small server needs. Everything else is policy.
The shape of it
Default-deny inbound, default-allow outbound, allow loopback, allow established connections, and then a short allow-list for what you publish. Stateful filtering does most of the heavy lifting for free.
table inet filter {
chain input {
type filter hook input priority 0;
ct state established,related accept
ct state invalid drop
iif lo accept
tcp dport { 22, 443 } accept
log level warning
drop
}
}Logging the drops
Add a final log-and-drop so you can see what got rejected. A quiet drop is safe; a logged drop is useful. I rate-limit the logging so a scan storm does not fill the disk.
log level warning prefix "nft-drop: " limit rate 5/minute
dropMake it survive reboot
Save the ruleset and restore it on boot, or use the nft systemd unit. A firewall that only exists until the next restart is a hobby.
If you cannot read your firewall ruleset out loud, you do not have a firewall — you have a hope.