Skip to content

Firewall (nftables / ufw)

Reference: Debian Wiki — Uncomplicated Firewall (ufw) · Debian Wiki — nftables · nftables Wiki

Host firewall vs network firewall

This covers host-level firewall rules (on the Linux machine itself). Your OPNsense router handles network-level filtering. Host firewall adds defense-in-depth — if a container exposes a port you forgot about, the host firewall is a second layer of protection.

What Debian 13 gives you out of the box

Nothing is filtered by default. The nftables package is installed but nftables.service is disabled and the ruleset is empty. ufw isn't installed. Pick one of the two below; running both means two tools fighting over the same tables.


ufw — Uncomplicated Firewall

ufw is the simplest firewall tool. It's a frontend that writes iptables rules (via iptables-nft on Debian, so they end up in nftables anyway). Install it with sudo apt install ufw; it's inactive until you ufw enable. Ubuntu ships it pre-installed, which is why so many guides use it.

Basic Setup

# Check current status
sudo ufw status
sudo ufw status verbose      # more detail
sudo ufw status numbered     # with rule numbers

# Enable / disable
sudo ufw enable
sudo ufw disable

# Reset all rules to default
sudo ufw reset

Warning

Before enabling ufw, always allow SSH first or you will lock yourself out.

sudo ufw allow ssh       # allow SSH before enabling
sudo ufw enable

Default Policies

# Deny all incoming, allow all outgoing (recommended starting point for servers)
sudo ufw default deny incoming
sudo ufw default allow outgoing

Allow Rules

# By service name (uses /etc/services)
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

# By port number
sudo ufw allow 22
sudo ufw allow 8080
sudo ufw allow 443

# By port and protocol
sudo ufw allow 22/tcp
sudo ufw allow 53/udp

# Port range
sudo ufw allow 8000:8100/tcp

# From a specific IP
sudo ufw allow from 192.168.1.0/24
sudo ufw allow from 192.168.1.50

# From specific IP to specific port
sudo ufw allow from 192.168.1.50 to any port 22
sudo ufw allow from 192.168.1.0/24 to any port 5984   # CouchDB — LAN only

Deny Rules

# Deny a port
sudo ufw deny 23            # block telnet

# Deny from an IP
sudo ufw deny from 192.0.2.50

# Deny from IP to specific port
sudo ufw deny from 192.0.2.50 to any port 22

Delete Rules

# By rule number (get numbers from 'ufw status numbered')
sudo ufw delete 3

# By matching the rule
sudo ufw delete allow 8080
sudo ufw delete allow from 192.168.1.50 to any port 22

App Profiles

ufw includes named profiles for common applications:

# List available profiles
sudo ufw app list

# View a profile's ports
sudo ufw app info nginx

# Allow by profile
sudo ufw allow 'Nginx Full'    # HTTP + HTTPS
sudo ufw allow 'Nginx HTTP'    # HTTP only
sudo ufw allow 'OpenSSH'

Useful ufw Commands

# Check if ufw starts on boot
sudo systemctl is-enabled ufw

# View ufw logs (kernel messages; /var/log/ufw.log only exists with rsyslog installed)
sudo journalctl -k -g UFW

# Enable logging
sudo ufw logging on
sudo ufw logging medium    # options: off, low, medium, high, full

nftables

nftables is the modern replacement for iptables and Debian's default firewall framework since Debian 10. The iptables command on Debian is iptables-nft, which writes nftables rules behind the scenes. On a Debian server, /etc/nftables.conf + nftables.service is the native way to keep a ruleset.

Basic Concepts

table    → top-level namespace (like a rulebook)
chain    → ordered list of rules within a table
rule     → individual match + action

Checking nftables

# View all current rules
sudo nft list ruleset

# View a specific table
sudo nft list table inet filter

# Flush all rules (careful — removes all filtering)
sudo nft flush ruleset

Basic Ruleset Example

sudo nano /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Accept established/related connections
        ct state established,related accept

        # Accept loopback
        iif lo accept

        # Accept ICMP (ping)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Accept SSH from LAN only
        ip saddr 192.168.1.0/24 tcp dport 22 accept

        # Accept HTTP and HTTPS
        tcp dport { 80, 443 } accept

        # Accept specific service ports from LAN
        ip saddr 192.168.1.0/24 tcp dport { 8080, 8090, 9898 } accept

        # Log and drop everything else
        log prefix "nftables drop: " drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

flush ruleset wipes Docker's rules too

Debian's stock /etc/nftables.conf (and the example above) starts with flush ruleset. On a Docker host that deletes the NAT and forwarding rules Docker created, and every published port and container's outbound traffic stops until you sudo systemctl restart docker. On Docker hosts, replace flush ruleset with table inet filter + flush table inet filter before the table definition so only your own table is reset, and never nft flush ruleset by hand.

# Apply rules
sudo nft -f /etc/nftables.conf

# Enable on boot
sudo systemctl enable --now nftables

# Test config syntax without applying
sudo nft -c -f /etc/nftables.conf

Docker & Firewall Interaction

Docker bypasses ufw

Docker directly modifies iptables/nftables and will expose ports even if ufw blocks them. A container with -p 0.0.0.0:8080:8080 is publicly accessible regardless of ufw rules.

Options to fix this:

Option 1 — Bind to localhost only (per container)

In your compose file, bind to 127.0.0.1 instead of all interfaces:

ports:
  - "127.0.0.1:8080:8080"   # only accessible locally

Option 2 — Use a dedicated Docker network + reverse proxy

Don't publish ports at all. Use an internal Docker network and let NPM/nginx handle external access via its own published ports.

Option 3 — Restrict via DOCKER-USER chain (iptables)

# Allow LAN access to Docker ports, block everything else
# (one rule: drop anything on eth0 that is NOT from the LAN — two separate -I rules would end up in the wrong order)
sudo iptables -I DOCKER-USER -i eth0 ! -s 192.168.1.0/24 -j DROP

This is the only iptables chain Docker won't overwrite. On Debian these rules are lost at reboot unless you persist them (iptables-persistent) or add them from a unit that runs after docker.service.

Docker 29's nftables backend

Docker Engine 29 added an experimental native nftables backend ("firewall-backend": "nftables" in daemon.json). It has no DOCKER-USER chain: you add your own table with base chains at a different priority instead. Docker owns its docker-bridges tables, so don't edit those. Until it's stable, the default iptables backend (via iptables-nft) is what I'd run.

Lessons learned with Docker hosts and the network: Networking & DNS.


Sources & further reading