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.
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¶
#!/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:
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.