# nftables: Modern Linux Firewall

LLMS index: [llms.txt](/en/llms.txt)

---

> [!WARNING]
> Before changing nftables, make sure you have physical or console access to the server. A misconfigured input chain can block SSH and lock you out.

nftables replaced iptables in the Linux kernel starting with version 3.13. If you're still writing rules in iptables style, it's time to reconsider. nftables performs better, has built-in dual-stack IPv4/IPv6 support, and lets you manage the entire ruleset as a whole instead of entering commands one by one.

## Why Migrate from iptables

iptables has fundamental design problems. Each table (filter, nat, mangle) is a separate rule set with its own semantics. There is no built-in support for simultaneous IPv4 and IPv6 handling—you end up writing two separate rule sets. Performance degrades with a large number of rules due to linear lookup.

nftables takes a different approach. All protocols work within a single data structure—the ruleset. The kernel compiles rules into efficient lookup structures. Atomic ruleset replacement eliminates race conditions during rule updates.

> [!WARNING]
> RHEL 8 and newer redirect iptables to nftables by default. Ubuntu 22.04 and later do the same. Check with `update-alternatives --display iptables`.

## Basic Commands: Viewing and Flushing Rules

The first command to memorize:

```bash
nft list ruleset
```

Output shows all tables, chains, and rules. Without tables, output is empty—this is normal.

Create a table named filter for packet handling:

```bash
nft add table inet filter
```

`inet` means the table handles both protocols. For IPv4-only use `ip`, for IPv6 use `ip6`.

Add a chain for incoming traffic:

```bash
nft add chain inet filter input { type filter hook input priority 0 \; policy accept \; }
```

Flags breakdown:

| Flag | Purpose |
|------|---------|
| `type filter` | Chain type—packet filtering |
| `hook input` | Attachment point—incoming packets |
| `priority 0` | Processing order relative to other hooks |
| `policy accept` | Default action—allow everything |

Flush rules in a chain:

```bash
nft flush chain inet filter input
```

Delete an entire table:

```bash
nft delete table inet filter
```

## Adding and Deleting Rules by Handle

Create several rules and inspect their handles:

```bash
nft add rule inet filter input tcp dport 22 accept
nft add rule inet filter input tcp dport 80 accept
nft add rule inet filter input tcp dport 443 accept
nft list ruleset
```

Output shows something like:

```
table inet filter {
    chain input {
        type filter hook input priority 0; policy accept;
        tcp dport 22 accept
        tcp dport 80 accept
        tcp dport 443 accept
    }
}
```

Handles are hidden by default. To work with specific rules:

```bash
nft -a list ruleset
```

The `-a` flag reveals the handle for each rule. Now you can delete by number:

```bash
nft delete rule inet filter input handle 3
```

> [!TIP]
> Handles change whenever you add or delete a rule. If your script modifies the ruleset, save `nft -a list ruleset` output to a file for tracking.

Add a rule with priority before existing ones—at the chain start:

```bash
nft insert rule inet filter input tcp dport 2222 accept
```

The `add` command appends to the end, `insert` adds at the start. For insertion at a specific position:

```bash
nft add rule inet filter input position 2 tcp dport 8080 accept
```

## Atomic Ruleset Replacement

Adding rules one by one creates a window where some rules are active and others are not yet applied. This is unacceptable for production.

The solution: write the complete ruleset to a file and load it atomically:

```bash
nft list ruleset > /etc/nftables.conf
```

`/etc/nftables.conf` is the standard location across most distributions. Now edit it:

```
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        
        ct state established,related accept
        ct state invalid drop
        
        iif lo accept
        
        tcp dport 22 accept
        tcp dport 80 accept
        tcp dport 443 accept
        
        counter drop
    }
}
```

Load it:

```bash
nft -f /etc/nftables.conf
```

> [!NOTE]
> `flush ruleset` clears everything before loading. If you need to append to existing rules, remove this line.

Check without applying:

```bash
nft -c -f /etc/nftables.conf
```

The `-c` flag performs syntax validation without changing state. Useful in CI/CD before deployment.

Enable persistence on systemd distros:

```bash
systemctl enable --now nftables
```

`nftables.service` loads `/etc/nftables.conf` on boot. After editing the file, `systemctl restart nftables` applies it.

## Forward Chain

If the host is not a router, leave `forward` empty with a drop policy. When IP forwarding is on, the minimum is established replies plus transit between interfaces:

```bash
nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
nft add rule inet filter forward ct state established,related accept
nft add rule inet filter forward iifname "eth0" oifname "eth1" accept
```

For debugging, log before the implicit drop:

```bash
nft add rule inet filter forward log prefix "nft-forward-drop: " level warn
```

Logs go to `journalctl -k` or `/var/log/kern.log`.

## Common Scenarios: Blocking Ports and IPs

**Block incoming connection from a specific IP:**

```bash
nft add rule inet filter input ip saddr 1.2.3.4 drop
```

**Block outgoing to a specific IP:**

```bash
nft add rule inet filter output ip daddr 5.6.7.8 drop
```

**Block an IP range (CIDR):**

```bash
nft add rule inet filter input ip saddr 10.0.0.0/8 drop
```

**Block a port for everyone:**

```bash
nft add rule inet filter input tcp dport 25 drop
```

**Allow a port only for a specific subnet:**

```bash
nft add rule inet filter input ip saddr 192.168.1.0/24 tcp dport 5432 accept
```

**Log dropped packets:**

```bash
nft add rule inet filter input counter drop
```

Counters appear in `nft list ruleset` output—they show packet and byte counts.

## NAT via Masquerading

For sharing a single IP with a local network:

```bash
nft add table ip nat
nft add chain ip nat postrouting { type nat hook postrouting priority 100 \; }
nft add rule ip nat postrouting ip saddr 192.168.0.0/24 masquerade
```

Masquerading automatically substitutes the external IP of the interface. For port forwarding NAT:

```bash
nft add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
nft add rule ip nat prerouting tcp dport 8080 dnat to 192.168.0.100:80
```

> [!WARNING]
> NAT in nftables works only for IPv4. For IPv6, use stateless NAT66 or routing-level addressing.

## iptables-nft Compatibility Mode

Some distributions keep iptables "working" by translating to nftables:

```bash
update-alternatives --set iptables /usr/sbin/iptables-nft
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
```

The problem is these are two different worlds. iptables-nft translates commands to nftables, but reverse compatibility does not work. Rules created via iptables will not appear directly in `nft list ruleset`.

> [!WARNING]
> Do not use iptables and nftables simultaneously. The result is unpredictable. Either fully migrate to nftables or stay on iptables. Check current mode: `iptables -V` shows whether iptables-legacy or iptables-nft is in use.

For migration from iptables, use the `iptables-translate` utility:

```bash
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
```

Output: `nft add rule ip filter input tcp dport 22 accept`. Manual verification of output is mandatory—automatic translation is not perfect.

nftables is not the future—it is the present. If you administer Linux servers, spend an evening on migration. The file `/etc/nftables.conf` with the complete ruleset is your backup and deployment in one.
