Security

Use nftables sets for fast allowlists and blocklists

Build named nftables sets for SSH allowlists and IP blocklists, use intervals for CIDR ranges and timeouts for temporary bans, change elements live, and rate-limit per IP with dynamic sets.

AdvancedLinux server6 min read

An nftables set is a named collection of addresses, ports or other values that one rule can match in a single fast lookup. Sets replace long chains of near-identical rules, and you can add or remove elements while the firewall runs. This guide adds three sets to a basic server ruleset: an SSH allowlist, a blocklist with CIDR ranges and temporary bans, and a dynamic set that rate-limits new SSH connections per source IP.

Before you start

  • A Linux VPS managed with plain nftables, not UFW or firewalld. If you have not built a ruleset yet, start with Get started with nftables.
  • Root or a sudo user. Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux or Rocky Linux 9 have recent enough nftables for every example here.
  • Your own public IP. Examples use 203.0.113.10.
  • Access to the browser console in your VPS provider's control panel.

Warning: An SSH allowlist blocks every address not in the set, including your home IP if it changes. Keep your SSH allow rule in place until you have tested the new one from a second session, and remember the browser console in your VPS provider's control panel works even when SSH is blocked.

Set basics

A set declaration names a type and optional flags:

  • type ipv4_addr or ipv6_addr for addresses, inet_service for ports.
  • flags interval lets the set hold CIDR ranges such as 198.51.100.0/24.
  • flags timeout lets each element expire on its own.
  • flags dynamic lets rules add elements from the packet path.
  • auto-merge joins overlapping or adjacent ranges instead of rejecting them.

A rule refers to a named set with @name, for example ip saddr @blocklist drop.

Add the sets to your ruleset

Edit your ruleset file (/etc/nftables.conf on Ubuntu and Debian, /etc/sysconfig/nftables.conf on AlmaLinux and Rocky Linux). This version extends the basic input chain:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    set ssh_allow {
        type ipv4_addr
        flags interval
        auto-merge
        elements = { 203.0.113.10, 198.51.100.0/28 }
    }

    set blocklist {
        type ipv4_addr
        flags interval
        auto-merge
    }

    set tempban {
        type ipv4_addr
        flags timeout
        timeout 1h
    }

    set ssh_meter {
        type ipv4_addr
        size 65535
        flags dynamic, timeout
        timeout 10m
    }

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

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

        ip saddr @blocklist drop
        ip saddr @tempban drop

        meta l4proto { icmp, ipv6-icmp } accept

        tcp dport 22 ct state new ip saddr != @ssh_allow drop
        tcp dport 22 ct state new update @ssh_meter { ip saddr limit rate over 10/minute burst 5 packets } add @tempban { ip saddr } drop
        tcp dport 22 accept

        tcp dport { 80, 443 } accept
    }

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

How it fits together:

  • Blocklist and tempban are checked early, so listed addresses never reach any service.
  • ssh_allow drops new SSH connections from anyone not on the list. Leave out that line if you want SSH open to everyone and rely on rate limiting only.
  • ssh_meter keeps one rate counter per source IP. An address that opens more than 10 new SSH connections a minute, after a burst of 5, is added to tempban for an hour.
  • Established connections are accepted first, so your current session is never cut by these rules.

Change 22 if SSH runs on another port. These sets cover IPv4 only: rules that match ip saddr ignore IPv6 packets, so SSH over IPv6 is not covered by the allowlist. If your server has IPv6, add matching ipv6_addr sets and rules using ip6 saddr.

Test and apply safely

Check the syntax first:

Bash
sudo nft -c -f /etc/nftables.conf

Schedule a rollback in case the allowlist is wrong, then load the file:

Bash
sudo systemd-run --on-active=5m --unit=nft-rollback /usr/sbin/nft flush ruleset
sudo nft -f /etc/nftables.conf

Open a new SSH session from your allowed IP. If it works, cancel the rollback:

Bash
sudo systemctl stop nft-rollback.timer

The automatic rollback guide explains this pattern in more detail.

Add and remove elements live

Changes made with nft take effect at once, with no reload:

Bash
# block one address and a whole range
sudo nft add element inet filter blocklist { 198.51.100.7, 192.0.2.0/24 }

# ban an address for 30 minutes instead of the default hour
sudo nft add element inet filter tempban { 198.51.100.8 timeout 30m }

# allow a new admin IP for SSH
sudo nft add element inet filter ssh_allow { 203.0.113.25 }

# remove entries
sudo nft delete element inet filter blocklist { 198.51.100.7 }

Live changes are lost when the ruleset file is reloaded. For permanent entries in ssh_allow or blocklist, add them to the elements line in the file too. tempban and ssh_meter are meant to be temporary, so they start empty on each load.

To load a long blocklist, keep it in a separate file and include it. For example, create /etc/nftables.d/blocklist.nft:

add element inet filter blocklist { 198.51.100.0/24, 192.0.2.0/25 }

Then add include "/etc/nftables.d/blocklist.nft" at the end of the main file, after the table is defined.

Check it works

List a set and its current elements:

Bash
sudo nft list set inet filter blocklist
sudo nft list set inet filter tempban

Elements in tempban show the time left before they expire. To test rate limiting, open many quick SSH connections from a test machine that is in ssh_allow but is not your main admin IP, then check that its address appears in tempban.

Troubleshooting

  • "conflicting intervals specified": you added overlapping ranges to an interval set without auto-merge. Add auto-merge to the set and reload.
  • "Could not process rule" on the update @ssh_meter line: your nftables or kernel is too old for per-element limits. Check nft --version, update the OS, or drop the rate-limit line and use Fail2ban instead.
  • Rate limiting bans everyone at once: the limit was written outside the set element, which makes it a single global counter. Keep limit rate inside the braces after ip saddr.
  • Added IPs vanish after reboot: they were added live only. Put them in the ruleset or an included file.
  • Locked out by the allowlist: log in through the browser console and run sudo nft add element inet filter ssh_allow { your.new.ip }, then update the file.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.