The scenario
You are managing a Fedora server or a hardened desktop. The default firewalld service handles basic zone routing, but you need to drop packets based on connection tracking states, rate-limit specific IPs, or route traffic through custom NAT rules. You run firewall-cmd --direct and realize the abstraction is getting in your way. You want to talk to the kernel's netfilter subsystem directly. You open a terminal and type nft, but the syntax feels alien compared to iptables. You need a clear, step-by-step path to building a working ruleset without breaking your SSH session.
What nftables actually does
The Linux kernel routes network packets through a series of hooks. Each hook decides whether to accept, drop, or modify a packet before it reaches your application. nftables is the modern userspace interface that talks to these hooks. It replaced iptables by consolidating IPv4, IPv6, and Ethernet filtering into a single unified language. Instead of managing separate tables for each protocol, you define a family and attach chains to specific kernel hooks. Think of it like a customs checkpoint. The inet family covers both IPv4 and IPv6. The input hook catches packets destined for the local machine. The forward hook handles packets passing through to another network. The output hook manages packets your own services send out. Every rule you write is evaluated in order. The first match wins. If no rule matches, the chain's default policy takes over.
Priority numbers control when your chain runs relative to other kernel subsystems. Lower numbers run first. A priority of 0 places your filter chain after most built-in routing decisions but before most application-level processing. This is the standard position for stateful packet filtering. You can verify hook behavior by checking the kernel documentation or testing with tcpdump. The kernel does not guess your intent. It follows the exact order you define.
Check your active hooks before you start writing rules. A misplaced priority can silently bypass your filters.
Building a ruleset from scratch
You will construct a minimal but secure filter table. This example drops all incoming traffic by default, allows established connections, permits SSH, and whitelists a local subnet. Run these commands carefully. A misplaced rule can lock you out of your machine.
Here is how to create the table and attach the input chain with a drop policy.
sudo nft add table inet filter
# Creates the container for your rules. inet covers both IPv4 and IPv6.
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
# Attaches the chain to the kernel input hook. priority 0 runs after most built-in checks.
# policy drop ensures unmatched packets are silently discarded.
You need to allow return traffic for connections you already initiated. Without this, your SSH session will die the moment you apply the drop policy. Connection tracking maintains a state table in kernel memory. It records the direction, protocol, and ports of active flows.
Here is how to permit established and related traffic.
sudo nft add rule inet filter input ct state established,related accept
# ct state tracks connection metadata. established allows return packets.
# related permits helper protocols like FTP data channels or ICMP errors.
# This rule must appear before your drop policy takes effect.
Now you will open specific ports and subnet ranges. Order matters. The kernel evaluates rules from top to bottom. Once a packet matches a rule, evaluation stops for that chain.
Here is how to whitelist SSH and your local network.
sudo nft add rule inet filter input tcp dport 22 accept
# Opens port 22 for SSH. Place this before broader accept rules.
sudo nft add rule inet filter input ip saddr 192.168.1.0/24 accept
# Whitelists your local LAN subnet. Adjust the CIDR to match your network.
# The ip prefix is required in inet chains to distinguish from IPv6.
You can inspect the current ruleset to verify the syntax before moving forward. The list command dumps the exact configuration the kernel is using.
Here is how to view your active configuration.
sudo nft list ruleset
# Dumps the active configuration. Check for typos in port numbers or CIDR blocks.
# Output shows the exact rule order the kernel will evaluate.
Save your rules to a file before you reboot. Runtime memory vanishes on power loss.
Making it survive a reboot
Commands typed into nft only live in the kernel's runtime memory. A reboot wipes them clean. You must save the configuration to disk and tell nftables to load it on boot. Fedora ships with a dedicated configuration directory for this exact purpose. The nftables.service systemd unit reads a single file during startup.
Here is how to export your current ruleset to the persistent configuration file.
sudo nft list ruleset > /tmp/my-ruleset.nft
# Dumps the active rules to a temporary file for editing.
sudo cp /tmp/my-ruleset.nft /etc/sysconfig/nftables.conf
# Copies the file to the standard Fedora persistence path.
sudo systemctl enable --now nftables.service
# Registers the service to load /etc/sysconfig/nftables.conf at boot.
The nftables.service unit reads /etc/sysconfig/nftables.conf during startup. If you edit the file later, you must reload the service or run sudo nft -f /etc/sysconfig/nftables.conf to apply changes. Never edit files in /usr/lib/systemd/ or /usr/lib/nftables/. Those paths ship with packages and get overwritten on updates. Always work in /etc/. The -f flag tells nft to flush the existing ruleset and load the file atomically. This prevents partial configurations from breaking your network stack.
Reload the service after every config change. Divergent runtime and persistent states cause silent failures.
Verify it worked
You need to confirm the kernel is actually enforcing your rules. The nft list ruleset command shows the active state, but you should also test traffic flow. Connection tracking and hook priorities can behave differently under load.
Here is how to check the active rules and test connectivity from a remote machine.
sudo nft list chain inet filter input
# Shows only the input chain rules. Verify the drop policy is listed at the bottom.
ssh -o ConnectTimeout=5 user@your-server-ip
# Tests SSH access. If it hangs, your drop policy is active but SSH is blocked.
# The timeout prevents your terminal from freezing indefinitely.
If SSH fails, you have a race condition or a missing rule. Reconnect via console or recovery mode and add the missing accept rule before the drop policy takes effect. Check journalctl -xeu nftables.service if the service fails to start. The logs will point to syntax errors in your configuration file. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type journalctl -xeu <unit> muscle-memory style. It saves time when debugging boot failures.
Run journalctl -xeu nftables.service before guessing. Read the actual error before rewriting your config.
Common pitfalls and what the error looks like
The most frequent mistake is locking yourself out by applying a drop policy before allowing SSH. The kernel does not warn you. It just stops responding. Another common error is mixing IPv4 and IPv6 syntax incorrectly. The inet family handles both, but you must specify the protocol when matching addresses.
You will see this error if you try to match an IPv4 address without the ip prefix in an inet chain.
Error: syntax error, unexpected saddr, expecting identifier or string or number or boolean or set or map or anonymous set or anonymous map or expression or statement or statement list or statement list end or statement list end or statement list end or statement list end
The parser expects a protocol qualifier. Change saddr 192.168.1.0/24 to ip saddr 192.168.1.0/24. For IPv6, use ip6 saddr. Another trap is forgetting the semicolon and backslash in inline chain definitions. The nft CLI requires \; to terminate statements inside curly braces. If you omit it, the parser throws a syntax error and refuses to load the chain.
A third issue is assuming rules apply globally. Rules only apply to the chain they are attached to. If you add a rule to the input chain, it will not affect forwarded traffic. You must create a separate forward chain and attach it to the forward hook. Test each chain independently. Use nft monitor to watch packets hit your rules in real time. It prints every packet evaluation as it happens. This is invaluable for debugging complex stateful rules.
Test with nft monitor before deploying to production. Real traffic reveals blind spots faster than static config reviews.
When to use nft directly vs alternatives
Use firewalld when you need quick zone management, rich rules, and a graphical interface for desktop users. Use ufw when you prefer a simple command-line wrapper that translates to iptables or nftables under the hood. Use nft directly when you need connection tracking states, rate limiting, NAT masquerading, or complex packet modification that abstractions hide. Use iptables only when you are maintaining legacy scripts or working on older kernels that lack nftables support. Stay on firewalld if your infrastructure relies on SELinux port labeling and dynamic service management.
Pick the tool that matches your complexity. Abstractions save time until they do not.