Firewalls & nftables - Practical Packet Filtering for Linux Servers

Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 2: Service Networking
Prerequisites: tcp-ip-fundamentals, ip-addressing-subnets
Time: 2 hours
Tags: nftables, firewall, netfilter, iptables, nat, port-forwarding, security, linux

Summary

Every Linux server has a packet filter built into the kernel โ€” the question is whether you configured it or left it wide open. This lesson covers what a firewall actually does, why nftables replaced iptables, the nftables object model (tables โ†’ chains โ†’ rules), and a complete copy-paste ruleset for an internet-facing server that allows only SSH, HTTP, and HTTPS. It finishes with NAT/port forwarding and how to make your ruleset survive reboots.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Firewall Concepts
  2. iptables vs nftables
  3. nftables Syntax: Tables, Chains, Rules
  4. A Practical Server Ruleset
  5. NAT and Port Forwarding
  6. Persistence
  7. Common Commands Cheat Sheet
  8. Troubleshooting & Common Pitfalls
  9. Key Takeaways
  10. Next Steps

Firewall Concepts

A network firewall inspects packets crossing a network interface and decides per packet: accept, drop, or reject.

The single most important idea is stateful inspection. The kernel's connection tracker (conntrack) keeps a table of every flow it has seen. That lets you write two rules instead of thousands:

  1. Allow packets belonging to connections you initiated (established, related).
  2. Drop everything else inbound by default.

This is why outbound-initiated traffic (updates, DNS replies, your SSH session's return packets) works even under a strict "default drop" policy.

Other vocabulary you'll meet:

Term Meaning
Hook Kernel checkpoint where filtering happens: prerouting, input, forward, output, postrouting
Policy A base chain's default verdict when no rule matches (drop/accept)
Conntrack states new, established, related, invalid, untracked
DNAT Rewrite destination address/port (inbound port forwarding)
SNAT / masquerade Rewrite source address (sharing one public IP among many hosts)

iptables vs nftables

iptables dates from the kernel's 2.4 era. nftables arrived in kernel 3.13 as its replacement, and is what modern distributions ship.

Aspect iptables nftables
Tables/chains Predefined (filter, nat; INPUT/FORWARD/OUTPUT always exist) You create exactly what you need
Families Separate tools per family (iptables vs ip6tables) One inet family handles IPv4+IPv6 together
Rule syntax Per-match extensions compiled into the kernel Unified expression language, most matches built-in
Sets ipset bolted on separately Native sets and maps (verdict maps)
Atomic updates Each rule change is a syscall Full ruleset replace atomically via nft -f

Compatibility notes:

nftables Syntax: Tables, Chains, Rules

Per the official wiki, unlike iptables there are no predefined chains โ€” you explicitly attach base chains of your own naming to Netfilter hooks:

# create a table (the container for everything else)
nft add table inet myfilter

# create a base chain hooked into input, priority 0, default drop
nft add chain inet myfilter input { type filter hook input priority 0 \; policy drop \; }

Anatomy of that line:

Rules then match expressions and end in a verdict:

nft add rule inet myfilter input iifname "lo" accept
nft add rule inet myfilter input tcp dport 22 accept
nft add rule inet myfilter input ip saddr 192.168.1.0/24 accept

Power features worth knowing early:

# sets โ€” match many values without many rules
tcp dport { 22, 80, 443 } accept

# named sets โ€” updatable at runtime
nft add set inet myfilter blocklist { type ipv4_addr \; flags interval \; }
nft add element inet myfilter blocklist { 203.0.113.0/24 }

# verdict maps โ€” dispatch by value
ct state vmap { established : accept, related : accept, invalid : drop }

# counters on any rule
tcp dport 22 counter accept

A Practical Server Ruleset

This is essentially the nftables wiki's "simple ruleset for a server", annotated. Save as /etc/nftables.conf and load with nft -f:

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

table inet filter {

    chain inbound_ipv4 {
        # rate-limited ping for diagnostics (commented = currently off)
        # icmp type echo-request limit rate 5/second accept
    }

    chain inbound_ipv6 {
        # REQUIRED for IPv6: neighbour discovery, otherwise connectivity breaks
        icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
        # icmpv6 type echo-request limit rate 5/second accept
    }

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

        # allow replies to our own outgoing connections, drop garbage
        ct state vmap { established : accept, related : accept, invalid : drop }

        # loopback (local services talking to themselves)
        iifname lo accept

        # jump to protocol-specific helper chains
        meta protocol vmap { ip : jump inbound_ipv4, ip6 : jump inbound_ipv6 }

        # THE actual services: SSH + web, IPv4 and IPv6 in one rule
        tcp dport { 22, 80, 443 } accept

        # optional: see what's being dropped
        # log prefix "[nftables] Inbound Denied: " counter drop
    }

    chain forward {
        # this host is not a router
        type filter hook forward priority 0; policy drop;
    }
}

Design points to internalize:

Harden SSH further once things work:

# rate-limit new SSH connections (allow bursts, stop brute force)
tcp dport 22 ct state new limit rate 6/minute accept
# or restrict management to your VPN/LAN subnet only
ip saddr 10.8.0.0/24 tcp dport 22 accept

Apply and verify:

sudo nft -f /etc/nftables.conf
sudo nft list ruleset

โš ๏ธ Never test firewall changes without a safety net. Schedule an auto-revert before applying:

sudo systemd-run --on-active=300 sh -c 'nft -f /etc/nftables.conf.known-good'

If you lock yourself out of SSH, the known-good ruleset returns in five minutes.

NAT and Port Forwarding

Filtering happens in input/forward; rewriting happens in the nat type chains at prerouting (DNAT) and postrouting (SNAT). NAT tables see each connection only once (first packet), so there's no need for conntrack rules here.

Port forwarding (DNAT)

Forward public port 2222 to internal host 192.168.1.50:22 โ€” requires forward policy allowing it too:

table inet nat {
    chain prerouting {
        type nat hook prerouting priority -100;

        iifname "eth0" tcp dport 2222 dnat to 192.168.1.50:22
        iifname "eth0" tcp dport { 80, 443 } dnat to 192.168.1.60
    }
}

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

        ct state established,related accept
        ip daddr 192.168.1.50 tcp dport 22 accept
        ip daddr 192.168.1.60 tcp dport { 80, 443 } accept
    }
}

Note: DNAT'd traffic traverses forward, not input โ€” your input chain's allowed ports are irrelevant to forwarded flows. Also ensure kernel forwarding is on: sysctl -w net.ipv4.ip_forward=1.

Internet sharing (masquerade)

Let a private LAN reach the internet through this server:

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

masquerade picks the outgoing interface's current IP automatically; use plain snat to <ip> when the address is static.

Persistence

Rules loaded with nft live in memory only. To survive reboot:

Debian/Ubuntu:

sudo systemctl enable nftables.service   # reads /etc/nftables.conf

RHEL/Fedora: same service, config typically at /etc/sysconfig/nftables.conf (or include your file from it).

Test persistence honestly: reboot, then confirm sudo nft list ruleset | grep -c tcp shows your rules โ€” not just that the service started.

Common Commands Cheat Sheet

nft list ruleset                          # dump everything currently loaded
nft list tables                           # just table names
nft list table inet filter                # one table
nft flush ruleset                         # wipe all rules (careful!)
nft delete table inet oldtable            # remove one table

nft add rule inet filter input tcp dport 8080 counter accept     # live append
nft insert rule inet filter input index 0 ip saddr 203.0.113.7 drop  # prepend

nft monitor trace                         # live packet tracing (best debugging tool)
nft -i                                    # interactive mode; avoids shell quoting issues
nft -c -f /etc/nftables.conf              # check file syntax WITHOUT applying

Troubleshooting & Common Pitfalls

Key Takeaways

Next Steps

Sources & Related

Sources consulted during research:

Related KB articles:

Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse