I was able to solve this with the help of Claude AI.
TL;DR
If you're running Raspberry Pi 5 + Raspberry Pi OS Trixie (Debian 13) + Docker + iptables-legacy
and UFW gives you:
ERROR: Could not load logging rules
Status: inactive
...on every ufw enable attempt, this post explains the root cause and the complete fix.
It took 15+ failed attempts and deep source code analysis to crack this.
Saving you the pain.
Environment
- Hardware: Raspberry Pi 5 (8GB, aarch64)
- OS: Raspberry Pi OS Lite 64-bit (Debian Trixie / Debian 13)
- Kernel: 6.12.75+rpt-rpi-2712
- UFW: 0.36.2
- iptables: 1.8.11 (legacy backend -- required for Docker compatibility)
- Docker: running, with multiple containers including gluetun/qBittorrent
Background -- Why iptables-legacy?
On Debian Trixie with kernel 6.12.x, the default iptables backend is nf_tables.
However, Docker + UFW combination on this kernel breaks under nf_tables.
The fix is to switch to iptables-legacy:
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
This is persistent across reboots. After switching, Docker works fine.
But then UFW breaks in a different way -- and that's what this post is about.
The Problem
After switching to iptables-legacy, sudo ufw enable consistently fails:
Command may disrupt existing ssh connections. Proceed with operation (y/n)? y
ERROR: Could not load logging rules
Status: inactive
UFW never becomes active. Every attempt fails with the same error.
What Does NOT Fix It
Before getting to the solution, here's what I tried that did not work:
update-alternatives to iptables-legacy (needed but insufficient)
- Adding/removing
BEGIN UFW AND DOCKER stub in after6.rules
- Removing
:ufw-user-input from after6.rules stub
- Disabling
nftables.service (was already disabled)
- Setting
IPV6=no in /etc/default/ufw (correct but insufficient alone)
sudo ufw logging off (doesn't persist through reset)
sudo ufw reset (clean slate but bug remains)
- Setting
LOGLEVEL=off in /etc/ufw/ufw.conf (correct but insufficient alone)
- Deleting the
.pyc compiled cache (cache regenerates)
- Patching
_get_logging_rules() to use -A instead of -I
- Adding
fail_ok=True to -F and -Z chain commands
- Removing
logging-deny references from rules files
- Removing
skip-to-policy references from rules files
- Running
ufw enable twice (required but insufficient alone)
The real fix requires all of the above working together, plus one final patch.
Root Cause Analysis
The error "Could not load logging rules" is generated by this code in
/usr/lib/python3/dist-packages/ufw/backend_iptables.py:
try:
self.update_logging(self.defaults['loglevel'])
except Exception:
err_msg = _("Could not load logging rules")
raise UFWError(err_msg)
This catches any exception from update_logging() and re-raises it.
Inside update_logging(), three separate operations fail under iptables-legacy:
Bug 1: -I (insert) on empty chains fails
_get_logging_rules() generates -I (insert) commands for logging chains.
iptables-legacy cannot insert into an empty chain:
iptables v1.8.1 (nf_tables): (null) failed (Operation not supported): chain foo
This was first documented in Debian bug #911986 (2018) and never fixed in UFW.
Bug 2: -Z (zero counters) on empty chains fails
update_logging() calls -Z to zero counters on logging chains.
Same issue -- iptables-legacy fails on -Z for empty chains.
Bug 3: Outer exception handler re-raises regardless of loglevel
Even with LOGLEVEL=off set, any exception from update_logging() is caught
and re-raised as "Could not load logging rules". The loglevel check comes too late.
Bug 4: Rules files contain references to non-existent chains
With IPV6=no or LOGLEVEL=off, several rules files still reference chains
that don't exist (ufw6-logging-deny, ufw-skip-to-policy-input, etc.),
causing iptables-restore to fail on load.
The Complete Fix
Step 1: Switch to iptables-legacy (if not already done)
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
Verify:
iptables --version # must show (legacy)
Step 2: Configure UFW settings
# Set IPV6=no (adjust if you need IPv6)
sudo nano /etc/default/ufw
# Change: IPV6=yes
# To: IPV6=no
# Set LOGLEVEL=off
sudo nano /etc/ufw/ufw.conf
# Add or change: LOGLEVEL=off
Step 3: Apply three patches to backend_iptables.py
Backup first:
sudo cp /usr/lib/python3/dist-packages/ufw/backend_iptables.py \
/usr/lib/python3/dist-packages/ufw/backend_iptables.py.bak
Patch 1: Change -I to -A in _get_logging_rules()
sudo python3 << 'EOF'
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'r') as f:
content = f.read()
old = "rules_t.append([c, ['-I', c, '-j', 'RETURN'], 'delete_first'])"
new = "rules_t.append([c, ['-A', c, '-j', 'RETURN'], 'delete_first'])"
if old in content:
content = content.replace(old, new)
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'w') as f:
f.write(content)
print("Patch 1 applied")
else:
print("Patch 1: pattern not found")
EOF
Patch 2: Add fail_ok=True to -F and -Z chain operations
sudo python3 << 'EOF'
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'r') as f:
content = f.read()
old = " self._chain_cmd(c, ['-F', c])\n self._chain_cmd(c, ['-Z', c])"
new = " self._chain_cmd(c, ['-F', c], fail_ok=True)\n self._chain_cmd(c, ['-Z', c], fail_ok=True)"
if old in content:
content = content.replace(old, new)
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'w') as f:
f.write(content)
print("Patch 2 applied")
else:
print("Patch 2: pattern not found")
EOF
Patch 3: Suppress logging errors when LOGLEVEL=off (the critical fix)
sudo python3 << 'EOF'
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'r') as f:
content = f.read()
old = """ else:
try:
self.update_logging(self.defaults['loglevel'])
except Exception:
err_msg = _("Could not load logging rules")
raise UFWError(err_msg)"""
new = """ else:
try:
self.update_logging(self.defaults['loglevel'])
except Exception:
if self.defaults.get('loglevel', 'off') != 'off':
err_msg = _("Could not load logging rules")
raise UFWError(err_msg)"""
if old in content:
content = content.replace(old, new)
with open('/usr/lib/python3/dist-packages/ufw/backend_iptables.py', 'w') as f:
f.write(content)
print("Patch 3 applied")
else:
print("Patch 3: pattern not found")
EOF
Delete the compiled cache:
sudo rm -f /usr/lib/python3/dist-packages/ufw/__pycache__/backend_iptables.cpython-313.pyc
Step 4: Clean logging chain references from rules files
# Remove ufw-logging-deny from before.rules
sudo bash -c "grep -v 'ufw-logging-deny' /etc/ufw/before.rules > /tmp/before.rules.tmp && cp /tmp/before.rules.tmp /etc/ufw/before.rules"
# Remove ufw6-logging-deny from before6.rules
sudo bash -c "grep -v 'ufw6-logging-deny' /etc/ufw/before6.rules > /tmp/before6.rules.tmp && cp /tmp/before6.rules.tmp /etc/ufw/before6.rules"
sudo chmod 640 /etc/ufw/before6.rules
# Remove logging-deny chain declarations from user.rules and user6.rules
sudo bash -c "grep -v 'ufw-logging-deny' /etc/ufw/user.rules > /tmp/user.rules.tmp && cp /tmp/user.rules.tmp /etc/ufw/user.rules"
sudo bash -c "grep -v 'ufw6-logging-deny' /etc/ufw/user6.rules > /tmp/user6.rules.tmp && cp /tmp/user6.rules.tmp /etc/ufw/user6.rules"
# Remove skip-to-policy references from after.rules and after6.rules
sudo bash -c "grep -v 'ufw-skip-to-policy' /etc/ufw/after.rules > /tmp/after.rules.tmp && cp /tmp/after.rules.tmp /etc/ufw/after.rules"
sudo bash -c "grep -v 'ufw6-skip-to-policy' /etc/ufw/after6.rules > /tmp/after6.rules.tmp && cp /tmp/after6.rules.tmp /etc/ufw/after6.rules"
Step 5: Verify rules files are clean
sudo bash -c "iptables-restore --test < /etc/ufw/before.rules" 2>&1
sudo bash -c "iptables-restore --test < /etc/ufw/user.rules" 2>&1
sudo bash -c "iptables-restore --test < /etc/ufw/after.rules" 2>&1
sudo bash -c "ip6tables-restore --test < /etc/ufw/before6.rules" 2>&1
sudo bash -c "ip6tables-restore --test < /etc/ufw/user6.rules" 2>&1
sudo bash -c "ip6tables-restore --test < /etc/ufw/after6.rules" 2>&1
All six commands should return no output (no errors).
Step 6: Enable UFW (requires two runs)
sudo ufw enable # First run -- may error but sets ENABLED=yes internally
sudo ufw enable # Second run -- succeeds
sudo ufw status # Confirm active with full rule list
Step 7: Restart Docker (CRITICAL if running Docker)
sudo systemctl restart docker
If any containers fail with iptables errors after this:
cd /path/to/your/compose && docker compose down && docker compose up -d
Important Caveats
UFW package upgrades will overwrite patches
If apt upgrade upgrades the UFW package, the patches to backend_iptables.py
will be overwritten. After any UFW upgrade, reapply all three patches and
run sudo ufw enable twice.
Never use sudo ufw reload
Use sudo ufw enable instead. Adding new ports doesn't require a reload:
sudo ufw allow 8080/tcp comment 'My Service'
# Rule is active immediately -- no reload needed
Correct startup order if running Docker
1. Verify iptables --version shows (legacy)
2. sudo ufw enable (twice if needed)
3. sudo systemctl restart docker
4. docker compose up -d
IPv6 note
This guide sets IPV6=no which is appropriate for a pure IPv4 homelab.
If you need IPv6, the patches still help but you may need additional investigation.
Why This Isn't Fixed Upstream
This bug has existed in various forms since 2018 (Debian bug #911986).
The UFW maintainer's position is that UFW should honor the update-alternatives
mechanism and not force iptables-legacy. The iptables maintainer's position
is that -Z on empty chains is an iptables regression. Neither side has
produced a fix. UFW 0.36.2 is the current version in Debian Trixie with
no newer version available.
Related Bugs
Tested on: Raspberry Pi 5 8GB, Raspberry Pi OS Trixie (Debian 13),
kernel 6.12.75+rpt-rpi-2712, UFW 0.36.2, iptables 1.8.11, April 2026
I was able to solve this with the help of Claude AI.
TL;DR
If you're running Raspberry Pi 5 + Raspberry Pi OS Trixie (Debian 13) + Docker + iptables-legacy
and UFW gives you:
...on every
ufw enableattempt, this post explains the root cause and the complete fix.It took 15+ failed attempts and deep source code analysis to crack this.
Saving you the pain.
Environment
Background -- Why iptables-legacy?
On Debian Trixie with kernel 6.12.x, the default iptables backend is nf_tables.
However, Docker + UFW combination on this kernel breaks under nf_tables.
The fix is to switch to iptables-legacy:
This is persistent across reboots. After switching, Docker works fine.
But then UFW breaks in a different way -- and that's what this post is about.
The Problem
After switching to iptables-legacy,
sudo ufw enableconsistently fails:UFW never becomes active. Every attempt fails with the same error.
What Does NOT Fix It
Before getting to the solution, here's what I tried that did not work:
update-alternativesto iptables-legacy (needed but insufficient)BEGIN UFW AND DOCKERstub inafter6.rules:ufw-user-inputfromafter6.rulesstubnftables.service(was already disabled)IPV6=noin/etc/default/ufw(correct but insufficient alone)sudo ufw logging off(doesn't persist through reset)sudo ufw reset(clean slate but bug remains)LOGLEVEL=offin/etc/ufw/ufw.conf(correct but insufficient alone).pyccompiled cache (cache regenerates)_get_logging_rules()to use-Ainstead of-Ifail_ok=Trueto-Fand-Zchain commandslogging-denyreferences from rules filesskip-to-policyreferences from rules filesufw enabletwice (required but insufficient alone)The real fix requires all of the above working together, plus one final patch.
Root Cause Analysis
The error "Could not load logging rules" is generated by this code in
/usr/lib/python3/dist-packages/ufw/backend_iptables.py:This catches any exception from
update_logging()and re-raises it.Inside
update_logging(), three separate operations fail under iptables-legacy:Bug 1:
-I(insert) on empty chains fails_get_logging_rules()generates-I(insert) commands for logging chains.iptables-legacy cannot insert into an empty chain:
This was first documented in Debian bug #911986 (2018) and never fixed in UFW.
Bug 2:
-Z(zero counters) on empty chains failsupdate_logging()calls-Zto zero counters on logging chains.Same issue -- iptables-legacy fails on
-Zfor empty chains.Bug 3: Outer exception handler re-raises regardless of loglevel
Even with
LOGLEVEL=offset, any exception fromupdate_logging()is caughtand re-raised as "Could not load logging rules". The loglevel check comes too late.
Bug 4: Rules files contain references to non-existent chains
With
IPV6=noorLOGLEVEL=off, several rules files still reference chainsthat don't exist (
ufw6-logging-deny,ufw-skip-to-policy-input, etc.),causing
iptables-restoreto fail on load.The Complete Fix
Step 1: Switch to iptables-legacy (if not already done)
Verify:
iptables --version # must show (legacy)Step 2: Configure UFW settings
Step 3: Apply three patches to backend_iptables.py
Backup first:
sudo cp /usr/lib/python3/dist-packages/ufw/backend_iptables.py \ /usr/lib/python3/dist-packages/ufw/backend_iptables.py.bakPatch 1: Change
-Ito-Ain_get_logging_rules()Patch 2: Add
fail_ok=Trueto-Fand-Zchain operationsPatch 3: Suppress logging errors when LOGLEVEL=off (the critical fix)
Delete the compiled cache:
Step 4: Clean logging chain references from rules files
Step 5: Verify rules files are clean
All six commands should return no output (no errors).
Step 6: Enable UFW (requires two runs)
Step 7: Restart Docker (CRITICAL if running Docker)
If any containers fail with iptables errors after this:
Important Caveats
UFW package upgrades will overwrite patches
If
apt upgradeupgrades the UFW package, the patches tobackend_iptables.pywill be overwritten. After any UFW upgrade, reapply all three patches and
run
sudo ufw enabletwice.Never use
sudo ufw reloadUse
sudo ufw enableinstead. Adding new ports doesn't require a reload:Correct startup order if running Docker
IPv6 note
This guide sets
IPV6=nowhich is appropriate for a pure IPv4 homelab.If you need IPv6, the patches still help but you may need additional investigation.
Why This Isn't Fixed Upstream
This bug has existed in various forms since 2018 (Debian bug #911986).
The UFW maintainer's position is that UFW should honor the
update-alternativesmechanism and not force iptables-legacy. The iptables maintainer's position
is that
-Zon empty chains is an iptables regression. Neither side hasproduced a fix. UFW 0.36.2 is the current version in Debian Trixie with
no newer version available.
Related Bugs
Tested on: Raspberry Pi 5 8GB, Raspberry Pi OS Trixie (Debian 13),
kernel 6.12.75+rpt-rpi-2712, UFW 0.36.2, iptables 1.8.11, April 2026