--- title: "How to Configure an nftables Firewall on Ubuntu 24.04" description: "Write a dual-stack nftables firewall on Ubuntu 24.04 with sets, SSH rate limiting and logging, apply it with a rollback timer and translate old iptables rules." category: "Network Configuration" language: en source: https://cubepath.com/docs/network-configuration/nftables-configuration-iptables-successor updated: 2026-09-25 --- # How to Configure an nftables Firewall on Ubuntu 24.04 nftables is the current packet filtering framework of the Linux kernel and the successor to iptables, ip6tables, arptables and ebtables. A single `nft` command manages IPv4 and IPv6 together, rules can match against sets of addresses or ports in one lookup, and a whole ruleset is loaded atomically from one file. In this tutorial you will write a dual-stack firewall for a web server in `/etc/nftables.conf` on Ubuntu 24.04, apply it with an automatic rollback, manage it at runtime with sets, add port forwarding and translate existing iptables rules. ## Prerequisites To follow this guide you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Debian 12 and Rocky Linux 9 ship the same `nftables.service`, although Rocky loads its configuration from `/etc/sysconfig/nftables.conf` and uses firewalld by default. - A non-root user with `sudo` privileges. - Access to the server console from your provider's panel, in case a mistake blocks SSH. > Warning: use only one firewall manager. UFW and firewalld create their own nftables tables, and Docker manages its own rules. This guide disables UFW. On a Docker host, do not use `flush ruleset`, since it would delete Docker's rules; keep your rules in your own table instead. ## Key nftables concepts | Concept | What it is | |---------|-----------| | Family | The traffic a table sees: `ip` (IPv4), `ip6` (IPv6), `inet` (both), plus `arp`, `bridge` and `netdev` | | Table | A container for chains, sets and maps. Unlike iptables, there are no predefined tables | | Base chain | A chain attached to a netfilter **hook** (`input`, `forward`, `output`, `prerouting`, `postrouting`) with a **priority** and a **policy** | | Rule | A list of match expressions followed by a verdict such as `accept` or `drop` | | Set | A named collection of addresses, ports or other values that rules can match in one lookup and that you can change at runtime | Using the `inet` family means one table and one set of rules filter both IPv4 and IPv6, instead of maintaining `iptables` and `ip6tables` separately. ## Step 1 - Installing nftables and disabling UFW Install the package, which provides the `nft` command and the `nftables.service` unit that loads `/etc/nftables.conf` at boot: ```bash sudo apt update sudo apt install nftables ``` Check the version: ```bash sudo nft --version ``` ```output nftables v1.0.9 (Old Doc Yak #3) ``` If UFW is active, disable it so it does not add its own rules alongside yours: ```bash sudo ufw disable ``` Look at what is loaded right now: ```bash sudo nft list ruleset ``` On a clean server with UFW disabled the output is empty or contains only leftover tables. They will be removed when you load your own configuration. ## Step 2 - Writing the firewall configuration Keep the whole firewall in `/etc/nftables.conf`, the file `nftables.service` loads. Open it: ```bash sudo nano /etc/nftables.conf ``` Replace its contents with this ruleset for a server that offers SSH, HTTP and HTTPS: ```text #!/usr/sbin/nft -f flush ruleset define SSH_PORT = 22 table inet filter { set web_ports { type inet_service elements = { 80, 443 } } set blocklist_v4 { type ipv4_addr flags interval } set blocklist_v6 { type ipv6_addr flags interval } set ssh_ratelimit_v4 { type ipv4_addr flags dynamic timeout 1m } set ssh_ratelimit_v6 { type ipv6_addr flags dynamic timeout 1m } chain input { type filter hook input priority filter; policy drop; iif "lo" accept ct state established,related accept ct state invalid drop ip saddr @blocklist_v4 drop ip6 saddr @blocklist_v6 drop # ICMPv6 is required for neighbour discovery and path MTU discovery meta l4proto ipv6-icmp accept icmp type echo-request limit rate 5/second burst 10 packets accept # SSH: drop sources that open more than 10 new connections per minute tcp dport $SSH_PORT ct state new add @ssh_ratelimit_v4 { ip saddr limit rate over 10/minute } drop tcp dport $SSH_PORT ct state new add @ssh_ratelimit_v6 { ip6 saddr limit rate over 10/minute } drop tcp dport $SSH_PORT ct state new accept tcp dport @web_ports ct state new accept udp dport 443 accept comment "HTTP/3 (QUIC)" limit rate 5/minute burst 10 packets log prefix "nft-drop: " level info } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; } } ``` How the ruleset works: - **`flush ruleset`** clears everything before loading, so running the file twice always gives the same result. The whole file is applied as one transaction: if any line has an error, nothing changes. - **`define`** creates a variable. Change `SSH_PORT` once if SSH listens on another port. - **Policies**: `input` and `forward` drop by default, `output` accepts, so the server can still reach update mirrors, DNS and APIs. - **Connection tracking**: packets of established connections are accepted early, which keeps the rest of the chain short. `related` also admits ICMP errors that belong to existing connections. - **Sets**: `web_ports` holds the ports to open, and the two `blocklist` sets are empty for now. The `interval` flag lets them hold whole networks such as `192.0.2.0/24`. - **SSH rate limiting**: each new SSH connection adds the source address to a dynamic set with its own rate counter. Once a source exceeds 10 new connections per minute, further attempts are dropped. Entries expire after one minute of inactivity. - **Logging**: the last rule logs packets that are about to hit the drop policy, limited so a flood cannot fill the logs. Remove the UDP 443 rule if your web server does not use HTTP/3. Check the syntax without applying it: ```bash sudo nft -c -f /etc/nftables.conf ``` The command prints nothing when the file is valid. Otherwise it points to the line and column of the error. ## Step 3 - Applying the ruleset with a rollback timer `nft` has no built-in confirmation step, so schedule a rollback before loading the new rules. This transient systemd timer flushes the ruleset in three minutes unless you cancel it, which reopens access if the new rules lock you out: ```bash sudo systemd-run --on-active=3m --unit=nft-rollback /usr/sbin/nft flush ruleset ``` ```output Running timer as unit: nft-rollback.timer Will run service as unit: nft-rollback.service ``` Load the configuration: ```bash sudo nft -f /etc/nftables.conf ``` Open a **new** SSH session from another terminal. Your current session is already established and does not prove the rules are correct. If the new session works, cancel the rollback: ```bash sudo systemctl stop nft-rollback.timer ``` Now enable the service so the file is loaded at every boot: ```bash sudo systemctl enable nftables.service ``` List the active ruleset to confirm it matches the file: ```bash sudo nft list chain inet filter input ``` From another machine, check that a port you did not open is now filtered, which shows up as a timeout: ```bash nc -zv -w 3 your_server_ip 3306 ``` ```output nc: connect to your_server_ip port 3306 (tcp) timed out: Operation now in progress ``` ## Step 4 - Managing sets and rules at runtime Sets let you change what the firewall allows without touching the rules. Open an extra port for everyone: ```bash sudo nft add element inet filter web_ports { 8443 } ``` Block an address or a whole network: ```bash sudo nft add element inet filter blocklist_v4 { 198.51.100.66, 192.0.2.0/24 } sudo nft add element inet filter blocklist_v6 { 2001:db8:bad::/48 } ``` Check the contents of a set and remove an entry: ```bash sudo nft list set inet filter blocklist_v4 sudo nft delete element inet filter blocklist_v4 { 198.51.100.66 } ``` You can also see which addresses the SSH rate limiter is currently tracking: ```bash sudo nft list set inet filter ssh_ratelimit_v4 ``` To delete a single rule you need its **handle**. List the chain with `-a`: ```bash sudo nft -a list chain inet filter input ``` ```output table inet filter { chain input { # handle 6 type filter hook input priority filter; policy drop; iif "lo" accept # handle 12 ct state established,related accept # handle 13 ... udp dport 443 accept comment "HTTP/3 (QUIC)" # handle 23 ``` ```bash sudo nft delete rule inet filter input handle 23 ``` > Important: changes made with `nft add` or `nft delete` are lost at the next reboot or `nft -f`. To keep them, add them to `/etc/nftables.conf` as well, for example as `elements = { ... }` in the set definition, and check the file with `sudo nft -c -f /etc/nftables.conf`. ## Step 5 - Reading logs and counters Log entries from the last rule go to the kernel log: ```bash sudo journalctl -k --grep 'nft-drop' -n 20 ``` ```output Sep 24 12:40:03 web01 kernel: nft-drop: IN=eth0 OUT= MAC=... SRC=192.0.2.200 DST=198.51.100.20 LEN=44 TOS=0x00 PREC=0x00 TTL=241 ID=50211 PROTO=TCP SPT=51544 DPT=23 WINDOW=1024 RES=0x00 SYN URGP=0 ``` To see how much traffic a rule matches, add the `counter` keyword to it in the configuration file, for example: ```text tcp dport @web_ports ct state new counter accept ``` After reloading, `sudo nft list chain inet filter input` shows `counter packets N bytes M` next to that rule. To reset all counters, run `sudo nft reset counters`. ## Step 6 - Forwarding a port to another host (optional) nftables handles NAT in a table with `nat` type chains. This example forwards TCP port 8080 arriving on `eth0` to a web server at `10.0.0.5:80` on a private network. Enable IP forwarding in the kernel: ```bash sudo nano /etc/sysctl.d/99-ip-forward.conf ``` ```ini net.ipv4.ip_forward = 1 ``` ```bash sudo sysctl --system ``` In `/etc/nftables.conf`, replace the empty `forward` chain so forwarded traffic to the backend is allowed: ```text chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept iif "eth0" ip daddr 10.0.0.5 tcp dport 80 ct state new accept } ``` Then append a NAT table at the end of the file: ```text table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; iif "eth0" tcp dport 8080 dnat to 10.0.0.5:80 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; ip daddr 10.0.0.5 tcp dport 80 masquerade } } ``` The `dnat` rule rewrites the destination of incoming packets, and `masquerade` rewrites their source to the gateway's address so the backend's replies return through it. Check and reload, then test from outside with `curl -I http://your_server_ip:8080`: ```bash sudo nft -c -f /etc/nftables.conf && sudo nft -f /etc/nftables.conf ``` ## Step 7 - Translating existing iptables rules The `iptables` package includes translators that print the nftables equivalent of iptables syntax. Translate one rule: ```bash iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT ``` ```output nft 'add rule ip filter INPUT tcp dport 22 ct state new counter accept' ``` Or a complete file saved with `iptables-save`: ```bash sudo iptables-save > iptables-backup.rules iptables-restore-translate -f iptables-backup.rules > translated.nft ``` Use the output as a starting point, not as a finished configuration. The translation keeps the iptables layout (separate `ip` and `ip6` tables, one rule per port), and extensions without an nftables equivalent are left as comments. Rewriting the result with an `inet` table and sets, as in Step 2, gives you a shorter and faster ruleset. ## Troubleshooting - **Locked out after loading rules**: wait for the rollback timer, or log in through the provider's console and run `sudo nft flush ruleset`, then fix the file. - **`Error: Could not process rule: No such file or directory`**: the table, chain or set you reference does not exist. Check the names with `sudo nft list ruleset`. - **Rules are gone after reboot**: `nftables.service` is not enabled, or the runtime changes were never written to `/etc/nftables.conf`. Run `systemctl is-enabled nftables`. - **IPv6 stops working**: ICMPv6 is dropped. Keep `meta l4proto ipv6-icmp accept` before any rule that drops traffic. - **UFW or Docker rules reappear**: another tool manages the firewall too. Run `sudo nft list tables` to see who created which table, and keep one manager. ## Conclusion You wrote a dual-stack nftables firewall with sets, SSH rate limiting and logging, applied it with a rollback timer, managed it at runtime and added optional port forwarding. As next steps, keep `/etc/nftables.conf` under version control, connect Fail2ban to nftables with its `nftables-multiport` action, and move any remaining iptables scripts over with `iptables-restore-translate`.