Firewall · nftables vs UFW
“UFW is not installed” does not mean you have no firewall (the Debian 13 trap)
2026-08-03 · HardStacked · 6 min read
You ran a security audit on a Debian 13 server and the report came back with “no
firewall detected”, or the slightly more honest variant, “UFW is not
installed”. Now you're wondering if your box has been sitting on the internet naked.
Short answer: probably not, and the alert is telling you more about the audit tool than
about your server.
1. What Debian actually ships
Debian 13 (trixie) does not install UFW by default. It never has. UFW is an Ubuntu
convenience wrapper, and a lot of tutorials written for Ubuntu get copy-pasted into
“Debian hardening guides” without anyone checking what Debian actually puts on
disk.
What Debian ships is nftables. On a fresh trixie install, the nftables package is present
and the nft binary is there. We verified this ourselves on a clean Debian 13 VM,
fully updated, zero modifications, while building our own audit tool: nftables present out of
the box, UFW absent. That is the normal, expected state of a Debian server.
So when an audit greps for the ufw binary, finds nothing, and prints
“no firewall”, it has tested for the presence of one specific Ubuntu-flavored
package and concluded something about your entire packet filtering capability. Those are not
the same question.
2. Check for yourself, in two minutes
Don't take the audit's word for it, and don't take mine either. Ask the kernel.
$ sudo nft list ruleset
This prints every nftables rule currently loaded. Not what's configured in some file, what
is actually active right now. Three outcomes:
Three outcomes, three different answers
- Command not found. Before you conclude anything, check your PATH.
nft lives in /usr/sbin, and depending on how you got your shell
(su without a login shell, some sudo configurations, a cron context),
/usr/sbin may not be in it. Try /usr/sbin/nft list ruleset. We
got bitten by exactly this while building our audit tool: the scan reported nft as absent
when it was sitting right there, outside the script's PATH. We now force a sane PATH at
the top of the scanner because of it. If the binary genuinely isn't on disk, that's
unusual for Debian 13 and worth investigating how the box was built.
- Empty output. The tool is installed but zero rules are loaded. This is the
default state of a fresh Debian install, and yes, it means no host firewall is currently
filtering. That is a real finding. But notice how different it is from “UFW is not
installed”: the correct response is “load rules into the framework Debian
gave you”, not “install a second framework”.
- Rules print out. You have a firewall. It might have been put there by an
nftables config, by firewalld, by fail2ban, by Docker, by your own past self. The audit
that said “no firewall” was flat wrong, and if you had followed its advice,
you'd have been about to stack a second ruleset on top of a working one. More on why
that's bad below.
Then check persistence:
$ systemctl status nftables
$ systemctl is-enabled nftables
$ cat /etc/nftables.conf
Rules loaded by hand with nft commands die at reboot. Rules in
/etc/nftables.conf with the nftables service enabled come back. A firewall that
evaporates on reboot is a firewall you don't have.
3. Why audit tools get this wrong
The root cause is worth understanding because it generalizes way past firewalls:
most audit checks test for the presence of a package instead of the presence of a
capability.
Testing “is the ufw binary on disk” is cheap and portable. Testing “is
traffic actually being filtered, by anything, right now” requires understanding that
Linux has one packet filter (the kernel's netfilter/nftables) and half a dozen frontends
that drive it: ufw, firewalld, iptables-nft, raw nft, plus daemons like Docker and fail2ban
that inject their own rules. A check written on Ubuntu, where ufw is the blessed frontend,
ships unchanged and produces a permanent false alarm on every Debian box it touches.
You'll find the same pattern all over security tooling. “Antivirus not
installed” on a box where the real question is whether anything scans anything.
“Fail2ban absent” on a host where sshd only accepts keys and the port is filtered
upstream anyway. Package presence is a proxy metric, and proxy metrics lie the moment you
change distributions.
When we hit this in our own tool, the fix wasn't to suppress the message. It was to make
the rule distro-aware: on Debian, ufw being absent is stated as a neutral fact, and the
actual judgment runs on nftables, with three distinct verdicts. nftables absent is not the
same finding as nftables present with zero rules, and neither is the same as rules loaded.
And when the tool can't check something at all, it says “not verified” instead of
“nothing found”. Those two get conflated constantly, and the difference matters:
“nothing detected” is a claim about your server, “not verified” is an
admission about the tool.
4. The trap: installing UFW to silence the alert
Here's where a false positive becomes a real incident. The audit says “install
ufw”, you apt install ufw && ufw enable, alert gone, ticket
closed.
On a box that already had nftables rules, you now have two rulesets in the same kernel.
nftables evaluates every base chain hooked at a given point, in priority order. An accept
verdict in one chain does not exempt the packet from the other chains, and a drop anywhere is
final. So your existing rules and ufw's rules are now both live, and the effective policy is
the intersection of two configurations that were each written assuming they were alone.
Two failure modes, pick your poison. Either ufw's default deny-incoming eats traffic your
existing rules were deliberately allowing, and if that traffic is SSH on a nonstandard port,
you've just locked yourself out of a remote server to fix a finding that was never real. Or
your existing permissive rules and ufw's rules interleave into something neither config
describes, and you walk away believing “ufw enable” means what the ufw docs say
it means, while the actual policy is something nobody has ever read. A firewall you
misunderstand is arguably worse than the empty ruleset you started with, because now you've
stopped looking.
If the ruleset genuinely was empty and you prefer ufw's ergonomics, fine, install it on
the empty box, it drives nftables underneath anyway. Just make that decision from
nft list ruleset output, not from an audit line written for a different
distribution.
5. What to actually verify
Forget “is package X installed”. Three questions, in order:
Are rules loaded? sudo nft list ruleset. Read it. If it's beyond you
today, at minimum identify the input chain's policy: policy drop with explicit
accepts is a firewall, policy accept with nothing else is decoration.
Do they survive a reboot? Service enabled, rules in the config file, and ideally
you've rebooted once and looked again. Assumption is not verification.
What do they let through, and to what? ss -tlnp shows what's actually
listening. Cross it against your rules. A perfect firewall in front of a service that
shouldn't be running is solving the wrong problem. And if you're on a cloud VPS, remember the
provider's security groups filter before your packets ever reach nftables; your host firewall
is the second layer, not the only one.
The deeper lesson
The tool we hit this in is audit-pi, and it's free:
github.com/Hardstacked/audit-pi, MIT.
On Debian it states ufw's absence as a neutral fact and runs the actual judgment on
nftables, with the three verdicts above kept separate. It also reports “nothing
detected” and “not verified” as two different things, because they are:
one is a claim about your server, the other an admission about the tool. Read the rule,
disagree with it, send a patch.
And if you've run the commands above and still aren't sure what your server is letting
through, write to me. Worst case you'll get a straight answer.