๐ŸŽ New User? Get 20% off your first purchase with code NEWUSER20 ยท โšก Instant download ยท ๐Ÿ”’ Secure checkout Register Now โ†’
Menu

Categories

nftables in Production: Rule Design and iptables Migration (2026)

nftables in Production: Rule Design and iptables Migration (2026)
nftables production rule design - Dargslan 2026

nftables has been the default packet-filtering framework on every major distribution for years - RHEL 8, Debian 10, Ubuntu 20.04 onward - and yet the rulesets in production are still mostly iptables ported over with iptables-translate. The translation works, but it produces flat, noisy rulesets that defeat the design that makes nftables genuinely better than its predecessor: one tool for IPv4 and IPv6, named sets and maps for O(1) lookups, atomic ruleset reload, named counters and meters that survive a flush, and a syntax that reads like a config file instead of a shell history. This guide shows the rule-design patterns that make nftables worth the migration, the production layout that scales from a single host to a fleet, and a practical iptables-to-nftables transition path. It ships with a free PDF cheat sheet of the syntax and commands you will reuse weekly.

Why nftables, not iptables-translate output

iptables-translate exists to ease the first hour of migration; it is not how anyone should ship nftables in production. The translated output preserves the iptables structure (one big chain per built-in hook, dozens of match options strung together, no use of sets), which means you keep the iptables maintenance burden while losing the readability that drew you to nftables in the first place. A real nftables ruleset uses the data model the framework was designed around: a small number of tables, a chain per logical purpose, sets and maps for everything that would have been a list of identical rules, a single counter you can name and reference, and atomic reload that swaps the entire ruleset in one transaction.

The operational payoff is concrete. Lookups against a 5,000-entry blocklist that took linear time in iptables take constant time in an nftables set. A ruleset that took 700 ms to flush-and-reload (a window during which the host was unfiltered) takes 4 ms to reload atomically. A change of one rule no longer requires reloading the surrounding 600. Logs reference named counters instead of "rule number 47". The ruleset is one file, version-controlled, that a colleague can read on day one.

The nftables data model

nftables introduces three primitives missing from iptables: tables, chains and sets/maps. A table belongs to one address family (ip, ip6, inet, arp, bridge, netdev) and is the unit of organisation. The inet family is special - it carries IPv4 and IPv6 in one place, eliminating the historical duplication.

Chains are either base chains (registered to a netfilter hook, like iptables' INPUT/OUTPUT/FORWARD) or regular chains (called by jumps from other chains, like iptables custom chains but cleaner). A base chain has a hook (input, output, forward, prerouting, postrouting), a priority, and a default policy. A regular chain has none of those - it is just a labelled block of rules.

Sets hold a list of values of one type (IPv4, IPv6, port, MAC, ifname, an interval). Maps hold key-value pairs - critical for things like "if source CIDR is X, jump to chain Y". Both can be flagged interval (for ranges), timeout (entries auto-expire), dynamic (rules can add to them), or counter (per-element hit counts). Sets and maps are the single most underused feature in real-world nftables, and the one that produces the biggest readability and performance wins.

A production-grade layout

The layout that scales: one inet filter table for IPv4/IPv6 packet filtering, base chains named exactly after their hook, regular chains for service-specific logic, sets for every reusable list of values. Skeleton:

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

table inet filter {
    set blocklist_v4 { type ipv4_addr; flags interval; }
    set blocklist_v6 { type ipv6_addr; flags interval; }
    set ssh_allow    { type ipv4_addr; flags interval;
                       elements = { 10.0.0.0/8, 192.0.2.5/32 } }

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

        ct state vmap { invalid : drop, established : accept, related : accept }
        iif lo accept

        ip  saddr @blocklist_v4 drop
        ip6 saddr @blocklist_v6 drop

        icmp type echo-request limit rate 5/second accept
        icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert } accept

        tcp dport 22 ip saddr @ssh_allow accept
        tcp dport { 80, 443 } accept

        log prefix "nft-input-drop: " level info limit rate 5/minute
        counter name input_drop drop
    }

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

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

This is twenty-something lines and covers the standard "single host, mixed v4/v6, SSH from internal only, public 80/443" case. Compare to the equivalent iptables ruleset (two parallel rule files, one per address family, dozens of rules each, a chain of -A commands) and the maintenance difference is obvious.

Sets, maps and the O(1) win

Anywhere you find yourself writing more than two rules that differ only by one literal, replace them with a set. Anywhere you find yourself writing a chain of "if X jump Y, if Z jump W" rules, replace them with a verdict map.

Example: a per-port verdict map that routes traffic to a service-specific chain:

map service_chain {
    type inet_service : verdict
    elements = {
        22  : jump ssh_chain,
        80  : jump http_chain,
        443 : jump http_chain,
        53  : jump dns_chain,
    }
}

chain input {
    ...
    tcp dport vmap @service_chain
    udp dport vmap @service_chain
    ...
}

Adding a service is one line in the map, not a rule plus a chain plus a re-test. The lookup is constant-time. The same pattern works for source-CIDR-based routing, MAC-based routing, ingress-interface-based dispatching - any "if attribute then verdict" decision becomes a one-line table.

Dynamic, timeout-bearing sets are the right tool for adaptive blocking: an SSH brute-force protection rule can populate a set with offending IPs and an automatic timeout - no fail2ban, no log scraping:

set ssh_brute {
    type ipv4_addr
    flags dynamic, timeout
    timeout 1h
    size 65535
}

chain input {
    ...
    tcp dport 22 ct state new add @ssh_brute { ip saddr limit rate over 5/minute } drop
    tcp dport 22 ct state new ip saddr @ssh_brute drop
    tcp dport 22 ct state new accept
    ...
}

Dual-stack in one ruleset

The inet family makes IPv4 and IPv6 first-class together. The trap is to write rules that quietly only apply to one family. The pattern: use family-agnostic matchers (tcp dport, ct state) wherever possible, and explicit ip saddr / ip6 saddr only where the address family genuinely matters. ICMP needs both icmp and icmpv6 matchers; the second is mandatory for IPv6 to function (Neighbor Discovery is part of ICMPv6).

Atomic reload

The single biggest operational win over iptables is atomic reload. nft -f /etc/nftables.conf applies the entire file in one transaction - either every rule activates together or none does. There is no window of partial filtering. For a host that legitimately has 600+ rules, this is the difference between a 0.7 second outage every reload and a no-impact reload. The pattern in production is to keep the canonical ruleset in /etc/nftables.conf, edit it, run nft -c -f /etc/nftables.conf (the -c is a syntax check without applying), then systemctl reload nftables which performs the atomic apply.

Named counters and meters

Counters are first-class objects; you can name them, reference them from multiple rules, and read them without flushing the ruleset:

counter input_drop  { }
counter ssh_attempt { }

chain input {
    tcp dport 22 ct state new counter name ssh_attempt accept
    counter name input_drop drop
}

# Read:
nft list counter inet filter input_drop
nft list counter inet filter ssh_attempt

Pair counters with the SIEM or Prometheus (the nftables_exporter reads named counters via the netlink API) and you have free per-rule visibility. meters are the rate-limited variant and are how you implement "block more than N packets per second" without an external tool.

iptables migration without downtime

The transition pattern that does not break production:

  1. Inventory. iptables-save > /tmp/iptables.v4, ip6tables-save > /tmp/iptables.v6. Read them. Document the intent of each rule, especially the ones that are no longer needed.
  2. Translate. iptables-restore-translate -f /tmp/iptables.v4 gives you nftables syntax. Do not stop here.
  3. Refactor. Restructure the translated output around inet filter, sets and maps, chains per service. The translated output is the starting point, not the destination.
  4. Test in parallel. Load the new ruleset in a second namespace or on a staging host. Replay traffic. Compare counters.
  5. Cutover. On the production host, save the working iptables rules, then nft -f new-ruleset.nft. The atomic apply means no flush window. Roll back is iptables-restore /tmp/iptables.v4.
  6. Disable iptables service. systemctl disable --now iptables ip6tables on RHEL-style; on Debian, mask the legacy service. Avoid running both - the rules layer in netfilter and the result is hard to reason about.

Common pitfalls

  • Default policy accept on the input chain. Set the policy to drop and only accept traffic you have decided to. Whitelist, not blacklist.
  • Forgetting the icmpv6 ND rules. Block them and IPv6 stops working. The standard set is nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, nd-router-solicit plus the echo types you want to allow.
  • Mixing legacy iptables and nftables on the same host. Pick one. The result of running both is non-deterministic from an ops point of view.
  • Reloading by flush + load instead of nft -f. The flush-then-load pattern reintroduces the iptables outage window. nft -f is atomic.
  • Burying logic in the file with no comments. nftables config supports # comments. Use them - the next operator on call is you in six months.

Audit checklist

  1. Single inet filter table covers IPv4 + IPv6 (1 pt)
  2. Default policy on input and forward is drop (1 pt)
  3. Reusable lists live in named sets, not as repeated rules (1 pt)
  4. Reload is atomic via nft -f, not flush + load (1 pt)
  5. Named counters exposed to the SIEM or Prometheus (1 pt)

5/5 = PASS, 3-4 = WARN, <3 = FAIL.

FAQ

Is iptables removed in modern kernels?

The legacy xt_* modules are still present for compatibility but distributions ship iptables-nft as the default iptables binary, which translates iptables commands into nftables rules under the hood. Long-term, the iptables command is going away; ship native nftables.

What about Docker and Kubernetes?

Both still emit iptables rules through the legacy ABI; on a recent kernel these land in the same nftables backend. Avoid editing those tables; keep your host firewall in a separate inet filter table to coexist cleanly.

Can I run firewalld?

Firewalld can use nftables as the backend, but it owns the ruleset and overrides hand edits. Either commit fully to firewalld zones or disable it and manage nftables.service directly - do not split.

Does ufw work with nftables?

ufw supports nftables as the backend on Ubuntu 21.10+. Same caveat as firewalld: pick one tool.

How do I monitor without polling?

The nftables_exporter for Prometheus reads named counters via the kernel's netlink API; ship the metrics and graph the rules you care about.

Related Dargslan resources

Share this article:
Dargslan Editorial Team (Dargslan)
About the Author

Dargslan Editorial Team (Dargslan)

Collective of Software Developers, System Administrators, DevOps Engineers, and IT Authors

Dargslan is an independent technology publishing collective formed by experienced software developers, system administrators, and IT specialists.

The Dargslan editorial team works collaboratively to create practical, hands-on technology books focused on real-world use cases. Each publication is developed, reviewed, and...

Programming Languages Linux Administration Web Development Cybersecurity Networking

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.