HardStacked
Share:

Attack surface · ufw

The firewall that reports green while blocking nothing

Published · HardStacked · 8 min

You ran ufw enable and got a wall of red: missing kernel module over and over, then RULE_APPEND failed line after line, IPv4 and then IPv6. You ran ufw status to see how bad it was. It answered Status: active.

Nothing crashed, and ufw is not lying to you. It is answering a much narrower question than the one you asked. The firewall is enabled. It is also empty.

The error wall in section 04 came off a hardened Debian machine on 2026-07-12. The ufw internals quoted in section 05 were read out of ufw 0.36.2-9 and iptables 1.8.11-2 on Debian 13 arm64, on 2026-08-15.

01. The two line verdict

ufw marked itself enabled and its chains were created. The rules that belong inside those chains never reached the kernel, because the netfilter match modules they depend on could not be loaded.

The status screen reads a text file. Only the kernel knows whether a packet gets dropped.

02. The fix

These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.

Three causes, in the order worth checking. The first one covers most machines.

The modules are simply not loaded. Load them yourself, then let ufw write its rules again:

bash
$ sudo modprobe xt_conntrack xt_addrtype && sudo ufw --force enable

If the wall goes away, you are done. Confirm with section 07 rather than with the status screen.

Module loading is locked for this boot. If modprobe answers Operation not permitted while you are already root, read this:

bash
$ sysctl kernel.modules_disabled

A 1 means the kernel will refuse every new module until the next boot, and the switch is one way by design: nothing can set it back to 0 while the machine is up. So there is no command that repairs the running system. What you can repair is the next boot, by having ufw come up before whatever sets the lock:

bash
$ sudo systemctl enable ufw

Then reboot, and count the rules instead of reading the status.

The modules are not on the machine at all. One command separates that case from the two above:

bash
$ ls /lib/modules/$(uname -r)/kernel/net/netfilter/ | grep -E 'xt_conntrack|xt_addrtype'

Two files listed means the modules exist and your problem is one of the first two. Nothing listed means your kernel was built or shipped without them, and no amount of ufw configuration will change that. Section 06 covers where that leaves you.

03. Why it happens

ufw does not match packets itself. It writes iptables rules, and most of those rules lean on a netfilter match module. The stock rule files shipped in the package use -m conntrack ten times and -m addrtype four times, so a missing module is not a cosmetic problem: it takes out entire rules.

xt_conntrack is the one that hurts. It backs the first rules ufw installs:

/usr/share/ufw/before.rules
# quickly process packets for which we already have a connection -A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT [...] # drop INVALID packets (logs these in loglevel medium and higher) -A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny -A ufw-before-input -m conntrack --ctstate INVALID -j DROP

Without it, ufw cannot express “let the replies to my own traffic back in”, and it cannot drop malformed state either. Every rule written below those assumes a firewall that has connection tracking.

xt_addrtype backs the ufw-not-local chain, which is how ufw tells a packet addressed to this machine apart from one addressed to a broadcast or a multicast group, through --dst-type LOCAL, MULTICAST and BROADCAST.

xt_mark deserves a warning, because it often shows up in the same wall and it will send you looking in the wrong place. ufw’s stock rules do not use -m mark anywhere. If your wall names it, some other rule is sharing your tables. Grepping ufw’s configuration for it is wasted time.

Now the mechanism. Netfilter match modules load on demand: the kernel pulls xt_conntrack in at the moment a rule that uses it is inserted. If ufw is inactive when the machine boots, no rule asks, so nothing loads. If something afterwards locks module loading, that on-demand path closes for the rest of the uptime, and the modules nobody needed earlier can no longer be reached at all.

Neither half is a defect. Leaving ufw off until it is configured is normal. Locking module loading on a running server is a standard hardening control, and a good one. The failure is in their meeting: a correct hardening step turns a recoverable gap into one that survives until the next reboot. Worth recognising as a shape, because neither half leaves a trace that points at the other.

04. What it looks like when you measure it

An excerpt of the wall. The full output repeats the same two messages for rule after rule, on both address families:

ufw --force enable
missing kernel module: xt_conntrack missing kernel module: xt_mark missing kernel module: xt_addrtype [...] RULE_APPEND failed RULE_APPEND failed

The appends failed in the IPv4 rule set, then again in the IPv6 rule set. On the same machine, in the same minute:

ufw status verbose
Status: active Default: deny (incoming)

That is the entire problem, in six lines. The first command says nothing was installed. The second says the firewall is up and denying.

05. The test that will lie to you

It is worth being exact about what ufw status checks, because it is far less than it looks like. Start with whether ufw believes it is on. In ufw/backend.py the function is six lines long, and its own docstring gives it away:

ufw/backend.py
def is_enabled(self): '''Is firewall configured as enabled''' if 'enabled' in self.defaults and \ self.defaults['enabled'] == 'yes': return True return False

Configured, not running. It returns true when ENABLED=yes sits in /etc/ufw/ufw.conf. No packet, no chain, no kernel.

ufw status goes exactly one step further. It asks the kernel a single yes or no question: does the chain ufw-user-input exist?

ufw/backend_iptables.py
# Is the firewall loaded at all? (rc, out) = cmd([self.iptables, '-L', \ 'ufw-user-%s' % (direction), '-n']) if rc == 1: return _("Status: inactive")

If the chain is missing you get Status: inactive. If it is there you get Status: active, and the list of rules printed underneath is then not read from the kernel at all. It is parsed from /etc/ufw/user.rules. The Default: deny (incoming) line in ufw status verbose is assembled the same way, out of /etc/default/ufw. Both are strings built from text files.

And a chain is not a rule. Creating the chain and appending into it are separate operations inside the same iptables-restore run. The chain declarations succeeded, the appends failed, and the green comes from the half that worked.

So ufw status proves that a chain exists. It never claimed anything more than that. We are the ones who read it as “the firewall is protecting me”.

06. If the modules are there and it still fails

Three situations where modprobe is not the answer. We have not reproduced these ourselves, so there is no output to show you here, only the mechanism.

You do not have your own kernel. On a containerised VPS, OpenVZ or LXC without the netfilter capabilities granted, you share the host kernel. Loading a module is a host operation, and your container is not permitted to request one. The module tree you can read belongs to the host and is read only from where you stand, so nothing you configure changes the outcome. What changes it is a KVM or bare metal instance, or a provider willing to load the matches on the host.

The provider kernel was built without them. Some hosts ship a trimmed kernel image. The listing in section 02 answers this in one line: if xt_conntrack.ko is not under /lib/modules/, there is nothing to load and nothing to unlock.

Two systems are writing the same tables. On Debian 13, iptables is a compatibility front end over nftables. Ask which binary you are actually running:

bash
$ update-alternatives --display iptables

If the machine also runs its own nft scripts, or if something still calls iptables-legacy, you have two writers on one engine. They do not merge, and the ruleset you read back is not necessarily the one you wrote last. ufw will keep reporting on its own files either way, which is what makes this one slow to find.

If the modules are present, module loading is unlocked and nothing else is writing your tables, then the problem is not ufw any more. It is the platform underneath it, and that is a longer evening than it looks from here.

07. Check that it is actually fixed

Do not re-read the status. Count what the kernel is actually holding:

bash
$ sudo iptables -S | grep -c '^-A ufw'

Then the same question for IPv6, which fails independently and is the half people forget:

bash
$ sudo ip6tables -S | grep -c '^-A ufw6'

You want two numbers comfortably above zero. A 0 sitting next to Status: active is the exact state this page is about, and counting is the only reading that settles it. ufw status answers “did I write my configuration file”. iptables -S answers “what will the kernel do to the next packet”. Only the second one stops anybody.

The same distance, between what a tool reports and what the machine does, turns up in a fail2ban jail that bans an address on the wrong port, in a WireGuard service that reports active with no interface at all, in a firewall the systemd unit reports as enabled while it is switched off, and in a hardened unit that runs as root and is refused with Permission denied.

Share: