# Build a server firewall with nftables

> Write a deny-by-default nftables ruleset for a Debian or Ubuntu server: established traffic, ICMP, SSH, web ports and allow-lists, applied with a rollback.

Difficulty: Intermediate\
Tested on: Debian 12, Debian 13, Ubuntu 24.04 LTS, Ubuntu 26.04 LTS

nftables is the packet filtering framework of current Linux kernels and the default firewall on Debian. Its configuration is one readable file that covers IPv4 and IPv6. This guide writes a deny-by-default ruleset for a server, applies it safely and shows how to extend it. Replace `198.51.100.7` with your own IP address.

## Before you start

- Log in with a sudo user and keep the web console ready if your service page shows one.
- Use **one** firewall tool. If UFW is active (`sudo ufw status`), disable it first with `sudo ufw disable`, or stay with UFW: [UFW firewall](/guides/ufw-firewall).
- Install the tools if needed (Debian includes them; on Ubuntu run `sudo apt install nftables`).

## Step 1: Write the ruleset

Open `/etc/nftables.conf`:

```bash
sudo nano /etc/nftables.conf
```

Replace its content with:

```text
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    set admin_hosts {
        type ipv4_addr
        flags interval
        elements = { 198.51.100.7 }
    }

    chain input {
        type filter hook input priority filter; policy drop;

        ct state established,related accept
        ct state invalid drop
        iif "lo" accept

        meta l4proto icmp accept
        meta l4proto ipv6-icmp accept

        tcp dport 22 accept
        tcp dport { 80, 443 } accept

        counter comment "dropped by policy"
    }

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

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

What it does: replies to your own connections are allowed, loopback and ICMP are allowed, SSH and web ports are open, everything else that arrives is dropped. The `admin_hosts` set is ready for allow-lists (Step 4).

## Step 2: Check the syntax

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

No output means the file is valid.

## Step 3: Apply with a safety net

Schedule an automatic rollback before you load the rules. If anything cuts your connection, the rules are flushed after five minutes:

```bash
sudo systemd-run --on-active=5min /usr/sbin/nft flush ruleset
sudo nft -f /etc/nftables.conf
```

Open a **new** SSH session. If it works, cancel the rollback: `systemctl list-timers` shows the timer (its name starts with `run-`), and `sudo systemctl stop` with that name cancels it.

Make the ruleset load at boot:

```bash
sudo systemctl enable nftables
```

**Verify:**

```bash
sudo nft list ruleset
```

## Step 4: Restrict a port to your addresses

To allow SSH only from the addresses in `admin_hosts`, change the SSH line to:

```text
tcp dport 22 ip saddr @admin_hosts accept
```

Add or remove addresses at runtime without reloading the whole file:

```bash
sudo nft add element inet filter admin_hosts { 203.0.113.0/24 }
sudo nft delete element inet filter admin_hosts { 203.0.113.0/24 }
```

Runtime changes are lost at reboot; add them to `/etc/nftables.conf` too. For IPv6 admin addresses, create a second set of `type ipv6_addr` and a matching rule with `ip6 saddr`.

## Step 5: Open more ports

Add a line per service in the `input` chain, check and reload:

```text
udp dport 51820 accept
tcp dport 25565 accept
```

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

## Log what is dropped

To see dropped packets, add a rate-limited log rule as the last line of the `input` chain:

```text
limit rate 5/minute log prefix "nft-drop: "
```

Read the log with `sudo journalctl -k | grep nft-drop`.

## Troubleshooting

**SSH froze after loading the rules.** Wait for the rollback timer, or use the console: `sudo nft flush ruleset`. Then fix the file. See [locked out after a firewall change](/guides/locked-out-after-firewall-change).

**IPv6 stopped working.** ICMPv6 is blocked. Keep the `ipv6-icmp` line.

**Rules disappear after a reboot.** The `nftables` service is not enabled, or another tool (UFW, Docker, firewalld) loads its own rules. Check `systemctl status nftables`.

**Outgoing mail on port 25 fails although the output chain accepts it.** Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request when you open a support ticket. Until then, send mail through a relay on port 587.

**Docker containers lose network access.** Docker manages its own rules; `flush ruleset` removes them. Restart Docker after reloading, or keep Docker's tables out of your flush as its documentation describes.

## Next steps

- Forward ports and use NAT: [ports and port forwarding](/guides/ports-and-port-forwarding).
- IPv6 on your server: [IPv6 setup](/guides/ipv6-setup).

## Frequently asked questions

### Should I use nftables or UFW?

UFW is easier for common cases. nftables gives you full control in one readable file, sets for allow-lists and NAT. Choose one; do not maintain rules in both.

### Do I need separate rules for IPv6?

No, if you use a table of family inet: its rules apply to IPv4 and IPv6. Remember to allow ICMPv6, which IPv6 needs to work.

### Are my rules kept after a reboot?

Yes, when they are in /etc/nftables.conf and the nftables service is enabled. Rules added only with the nft command are lost at reboot.

### Why allow ICMP at all?

ICMP carries error messages that keep connections working, such as path MTU discovery, and IPv6 needs ICMPv6 for neighbour discovery. Blocking it causes hard-to-find problems.

### How do I see which rule blocks traffic?

Add counters or a log statement to the rule, for example log prefix followed by a text, then read the kernel log with journalctl -k.

---

Source: <https://hyperdc.com/guides/networking/nftables-firewall>\
Updated: 2026-10-09
