Knowledgebase › nftables practical recipes — the UFW alternative for power users

nftables practical recipes — the UFW alternative for power users

UFW is great for "open SSH, open HTTPS, close everything else." When you need more control — per-source-IP rate limits, custom port ranges, blocking specific countries, dynamic sets — nftables is what's underneath UFW anyway, and writing it directly gives you that control. This article is a recipes collection.

The mental model

nftables organises rules in:

  • Tables — namespaces by address family (inet covers IPv4+IPv6 in one table).
  • Chains — hook points (input, output, forward, prerouting, postrouting).
  • Rules — match conditions and actions.
  • Sets — named collections of IPs/ports for use in rules.

Tables and chains are administrative; rules and sets do the work.

Recipe 1: Minimal sensible firewall

The "open SSH + HTTPS, drop everything else" baseline:

# /etc/nftables.conf
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Loopback always
        iif lo accept

        # Established and related connections
        ct state established,related accept

        # Drop invalid
        ct state invalid drop

        # ICMP (allow ping but rate-limit)
        ip protocol icmp limit rate 5/second accept
        ip6 nexthdr ipv6-icmp accept

        # Services
        tcp dport 22 accept comment "SSH"
        tcp dport { 80, 443 } accept comment "HTTP/HTTPS"
    }

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

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

Activate:

nft -f /etc/nftables.conf
systemctl enable --now nftables

Recipe 2: Per-source rate limiting

Limit SSH connection attempts to slow down brute force:

# In the input chain
tcp dport 22 ct state new \
    meter ssh_rate { ip saddr limit rate 5/minute } accept

tcp dport 22 drop  # Anything over the rate

The meter creates a per-source-IP counter; rate is measured per source. Add to nftables.conf and reload.

Recipe 3: Allowlist by source IP

For services that should only be reachable from known networks (admin panels, RDP, internal API):

# Define a set
set admin_sources {
    type ipv4_addr
    elements = { 203.0.113.0/24, 198.51.100.10 }
}

# Use it
tcp dport 8443 ip saddr @admin_sources accept comment "Admin UI"
tcp dport 8443 drop

Dynamic sets work too — for example, add/remove members at runtime from a script.

Recipe 4: Country blocking by GeoIP

nftables doesn't natively know about GeoIP. Two paths:

  • Pre-built set: download CIDR ranges for the countries you want to block, load into an nftables set. Refresh periodically via cron.
  • xt_geoip + xtables-compat: traditional approach via the legacy iptables compatibility layer.

Set-based approach:

# Generated CIDR list from MaxMind GeoLite or ipdeny.com
set blocked_countries {
    type ipv4_addr
    flags interval
    auto-merge
    elements = {
        # ... thousands of CIDRs ...
    }
}

# Rule
ip saddr @blocked_countries drop

Use sparingly. Country blocking is a blunt tool, often blocks legitimate users behind VPNs, and rarely stops a determined attacker.

Recipe 5: Block known bad actors dynamically

Add IPs to a set at runtime (e.g., from fail2ban or your own detection):

# Set with timeout — IPs auto-expire
set bad_actors {
    type ipv4_addr
    timeout 24h
}

# Drop traffic from anyone in the set
ip saddr @bad_actors drop

Then from a script or fail2ban action:

nft add element inet filter bad_actors { 203.0.113.5 timeout 24h }

The IP is dropped for 24 hours, then automatically removed from the set.

Recipe 6: NAT for outbound from a VPN subnet

You're running WireGuard (10.0.0.0/24) and want traffic from peers to NAT through the VPS's public IP:

table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        ip saddr 10.0.0.0/24 oifname "eth0" masquerade
    }
}

And enable IP forwarding (in /etc/sysctl.conf: net.ipv4.ip_forward=1).

Recipe 7: Port forwarding to a backend

Forward incoming traffic on port 8080 to a backend at 192.168.1.50:80:

table inet nat {
    chain prerouting {
        type nat hook prerouting priority -100;
        tcp dport 8080 dnat to 192.168.1.50:80
    }

    chain postrouting {
        type nat hook postrouting priority 100;
        ip daddr 192.168.1.50 masquerade
    }
}

Recipe 8: Rate-limit a specific service

Cap inbound HTTPS to 100 connections/second across all sources (DDoS mitigation lite):

tcp dport 443 ct state new limit rate 100/second accept
tcp dport 443 drop

For per-source rate limit, use the meter pattern from Recipe 2.

Recipe 9: Log dropped packets selectively

For diagnosing what's being blocked:

# Add before the policy drop
log prefix "nft-drop-input: " level info flags ip options
counter drop

Watches: journalctl -k -f | grep nft-drop. Don't leave this on in production — it generates a lot of log entries when you're under attack.

Recipe 10: IPv6 considerations

The inet table type handles IPv4 and IPv6 in one ruleset. For IPv6-specific rules:

# ICMPv6 — much more important than ICMPv4. Allow:
ip6 nexthdr ipv6-icmp icmpv6 type {
    destination-unreachable,
    packet-too-big,
    time-exceeded,
    parameter-problem,
    echo-request,
    echo-reply,
    nd-router-solicit,
    nd-router-advert,
    nd-neighbor-solicit,
    nd-neighbor-advert
} accept

Blocking ICMPv6 indiscriminately breaks IPv6 in subtle ways (path MTU discovery, neighbor discovery). Always permit the above message types.

Inspecting current ruleset

# Show full ruleset
nft list ruleset

# Show specific table
nft list table inet filter

# Show counters
nft -a list table inet filter   # also shows handle IDs

Removing a rule at runtime

# Find the handle
nft -a list table inet filter
# Note the rule's "handle N"

# Delete it
nft delete rule inet filter input handle N

Persisting

Rules added with nft add are not persistent across reboots. To persist:

  • Edit /etc/nftables.conf and reload: nft -f /etc/nftables.conf.
  • Or dump the current ruleset to the file: nft list ruleset > /etc/nftables.conf.

Enable the systemd unit (systemctl enable nftables) so the file is loaded at boot.

UFW vs raw nftables

UFW is excellent for the "I want SSH and HTTPS" case. Drop to nftables when:

  • You need rate limits.
  • You need source-IP allowlists.
  • You're running a router-like setup (NAT, forwarding).
  • You want to integrate with detection systems (fail2ban, custom scripts).

Don't run both at once — UFW manages nftables under the hood, and direct nft commands can conflict.

Also Read

« « Back

Powered by WHMCompleteSolution