Skip to content

# UFW "Could Not Load Logging Rules" on Raspberry Pi 5 + Debian Trixie + Docker + iptables-legacy ## A Complete Fix for a Deep Bug in UFW 0.36.2 #16215

Description

@SpawnZilla

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:

  1. update-alternatives to iptables-legacy (needed but insufficient)
  2. Adding/removing BEGIN UFW AND DOCKER stub in after6.rules
  3. Removing :ufw-user-input from after6.rules stub
  4. Disabling nftables.service (was already disabled)
  5. Setting IPV6=no in /etc/default/ufw (correct but insufficient alone)
  6. sudo ufw logging off (doesn't persist through reset)
  7. sudo ufw reset (clean slate but bug remains)
  8. Setting LOGLEVEL=off in /etc/ufw/ufw.conf (correct but insufficient alone)
  9. Deleting the .pyc compiled cache (cache regenerates)
  10. Patching _get_logging_rules() to use -A instead of -I
  11. Adding fail_ok=True to -F and -Z chain commands
  12. Removing logging-deny references from rules files
  13. Removing skip-to-policy references from rules files
  14. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions