Firewalls and nftables
nftables organises rules into tables and chains hooked into netfilter points (input, forward, output). A default-deny inbound policy drops everything unless explicitly permitted. firewalld is a higher-level manager over the same nftables kernel subsystem.
A freshly provisioned VM with no firewall rules accepts connections on every open port — including ports that services bind to by default (postgres on 5432, redis on 6379) because the developer assumed “it only listens on localhost.” But ss -tlnp shows 0.0.0.0:6379 and there are no nftables rules. Anyone on the same network segment can connect. The right mental model is default-deny: drop everything inbound and explicitly permit only what you intend to expose. nftables is the kernel’s packet filter that enforces this. firewalld is the higher-level tool that most RHEL-family admins use on top of it. Understanding the nftables object model — tables, chains, hooks, rules — is what lets you read, debug, and write firewall policy without cargo-culting rules you don’t understand. Threat-modeling and hardening posture live in the security-defensive track; this lesson covers the mechanics.
After this lesson you can describe the nftables object model (tables → chains → rules, chains hooked into netfilter), write a default-deny inbound ruleset that permits SSH, read an existing ruleset with nft list ruleset, understand what firewalld adds over raw nftables, and explain the input/forward/output hook distinction.
The nftables object model: tables, chains, hooks, rules.
nftables replaces iptables, ip6tables, arptables, and ebtables with a single unified framework. Every piece of policy lives in this hierarchy:
# List the entire current ruleset:
sudo nft list ruleset
# If nothing is configured, output is empty.
# A populated ruleset looks like:
# table inet filter {
# chain input {
# type filter hook input priority filter; policy drop;
# iif "lo" accept
# ct state established,related accept
# tcp dport 22 accept
# }
# chain forward {
# type filter hook forward priority filter; policy drop;
# }
# chain output {
# type filter hook output priority filter; policy accept;
# }
# }
# Object model:
# table = namespace; family = inet (IPv4+IPv6), ip, ip6, arp, bridge
# chain = ordered list of rules; attached to a netfilter hook
# hook = where in the kernel's packet path the chain fires
# input = packets destined for this host
# forward = packets being routed through this host
# output = packets originating from this host
# prerouting / postrouting = before/after routing decision (for NAT)
# rule = match + verdict (accept / drop / reject / jump / goto)
# policy = default verdict when no rule matches (accept or drop)Writing a minimal default-deny inbound ruleset.
# Create a table (inet = applies to both IPv4 and IPv6):
sudo nft add table inet filter
# Add the input chain with default-deny policy:
sudo nft add chain inet filter input \
'{ type filter hook input priority 0; policy drop; }'
# Rule 1: always accept loopback (lo) traffic — breaking this breaks localhost:
sudo nft add rule inet filter input iif lo accept
# Rule 2: accept established/related connections (return traffic for outbound):
sudo nft add rule inet filter input ct state established,related accept
# Rule 3: accept new SSH connections:
sudo nft add rule inet filter input tcp dport 22 accept
# Rule 4 (optional): accept a web server:
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
# Add output and forward chains (permissive output is typical for a server):
sudo nft add chain inet filter forward \
'{ type filter hook forward priority 0; policy drop; }'
sudo nft add chain inet filter output \
'{ type filter hook output priority 0; policy accept; }'
# Verify the full ruleset:
sudo nft list rulesetThe ct state established,related accept rule is critical — without it, your SSH session’s return packets (kernel replies to the client’s TCP segments) are dropped by the default-deny policy, and the connection stalls immediately after the rule is installed.
Persisting nftables rules across reboots.
Rules added with nft add live only in kernel memory. On reboot they vanish.
# Save current ruleset to a file:
sudo nft list ruleset > /etc/nftables.conf
# The file is a valid nft script. Contents will look like:
# #!/usr/sbin/nft -f
# table inet filter {
# chain input { ... }
# chain forward { ... }
# chain output { ... }
# }
# Load a ruleset from a file (replaces current rules):
sudo nft -f /etc/nftables.conf
# Enable the systemd service to load rules at boot:
sudo systemctl enable nftables
sudo systemctl start nftables
# systemd's nftables.service runs: nft -f /etc/nftables.conf at startup
# Test a ruleset file without applying it:
sudo nft -c -f /etc/nftables.conf
# -c = check only, do not loadThe nftables.service on Debian/Ubuntu loads /etc/nftables.conf by default. Check the service file if your distro uses a different path: systemctl cat nftables.
firewalld: a higher-level manager over nftables.
On RHEL/Fedora and increasingly on Debian, firewalld provides a zone-based abstraction over nftables (it used to manage iptables; since firewalld 0.6 it targets nftables by default).
# firewalld concepts:
# zone = a trust level applied to an interface or source address
# (public, internal, trusted, drop, block, work, home, dmz, external)
# service = a named port/protocol set (ssh = tcp/22, http = tcp/80, etc.)
# Check status:
sudo firewall-cmd --state
# running
# List active zones and their interfaces:
sudo firewall-cmd --get-active-zones
# public
# interfaces: ens3
# See what is allowed in the public zone:
sudo firewall-cmd --zone=public --list-all
# public (active)
# target: default
# interfaces: ens3
# services: dhcpv6-client ssh
# Permanently allow HTTP (--permanent writes to XML config):
sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --reload
# Allow a specific port:
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
sudo firewall-cmd --reload
# Do NOT mix raw nft rules with firewalld — firewalld rebuilds the nftables
# ruleset on reload and will overwrite any rules you added manually.
sudo nft list ruleset | head -5
# table inet firewalld { ... } ← firewalld owns this tableReading and debugging an existing ruleset.
# Full ruleset dump — the ground truth:
sudo nft list ruleset
# List just one table:
sudo nft list table inet filter
# List one chain:
sudo nft list chain inet filter input
# Add a rule with a counter to see how many packets match:
sudo nft add rule inet filter input tcp dport 443 counter accept
sudo nft list chain inet filter input
# tcp dport 443 counter packets 847 bytes 51234 accept
# Delete a specific rule by its handle (handle shown with --handle flag):
sudo nft list chain inet filter input --handle
# tcp dport 22 accept # handle 3
sudo nft delete rule inet filter input handle 3
# Flush (delete all rules in) a chain without removing the chain itself:
sudo nft flush chain inet filter input
# Delete an entire table:
sudo nft delete table inet filterLocking down a new VM: allow only SSH and HTTP, deny everything else inbound.
# Starting from a clean state (no existing rules):
sudo nft list ruleset
# (empty)
# Build the ruleset:
sudo nft add table inet filter
sudo nft add chain inet filter input \
'{ type filter hook input priority 0; policy drop; }'
sudo nft add chain inet filter forward \
'{ type filter hook forward priority 0; policy drop; }'
sudo nft add chain inet filter output \
'{ type filter hook output priority 0; policy accept; }'
# Essential rules — add in order (evaluated top to bottom):
sudo nft add rule inet filter input iif lo accept
sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input ct state invalid drop
sudo nft add rule inet filter input tcp dport 22 accept
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
# Verify: does SSH still work?
# (test from another terminal BEFORE saving — if this shell hangs, you locked yourself out)
ssh 10.0.0.10 echo ok
# ok
# Save and enable:
sudo nft list ruleset > /etc/nftables.conf
sudo systemctl enable --now nftables
# Confirm: postgres on 5432 is now unreachable from outside,
# even though it binds 0.0.0.0:5432 — the input chain drops the SYN.The ct state invalid drop rule catches malformed packets and TCP sequence-number attacks before they hit the accept rules — a common hardening addition.
▸Why this works
This lesson bridges to the security-defensive track. The mechanics covered here — default-deny, chain policy, permit by exception — are the packet-filter foundation. The security-defensive track covers what goes on top: threat modeling to decide which ports to expose, rate-limiting rules to mitigate brute-force, logging dropped packets for incident response, and hardening posture (fail2ban, intrusion detection). Knowing nftables mechanics makes those higher-level decisions legible; skipping the mechanics means you are copying firewall rules you cannot debug.
▸Common mistake
Applying a default-deny input policy before adding the established/related rule. If you run sudo nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }' before adding ct state established,related accept, your existing SSH session stops receiving return packets and hangs within seconds. Always write the full ruleset to a file and load it atomically with nft -f /etc/nftables.conf, or add the established/related rule as the very first action before changing the chain policy. On a remote server, test with a second terminal open before saving.
Your nftables input chain has policy drop and these rules in order: (1) iif lo accept, (2) ct state established,related accept, (3) tcp dport 22 accept. A new inbound TCP SYN arrives on port 5432. What happens?
nftables is the Linux kernel’s packet filtering framework, replacing iptables. Its object model: tables (namespaces, family inet covers IPv4+IPv6) contain chains that are hooked into netfilter points — input (destined for this host), forward (transiting through), output (locally generated). Each chain holds ordered rules; when no rule matches, the chain policy (accept or drop) fires. A default-deny inbound policy (policy drop) means only explicitly permitted traffic gets through. The critical companion rule is ct state established,related accept — without it, return packets for outbound connections are dropped. Persist rules with nft list ruleset > /etc/nftables.conf and systemctl enable nftables. firewalld is a zone-based manager that generates and owns nftables rules — never mix manual nft commands with firewalld on the same host. Threat modeling, rate-limiting, and hardening posture are covered in the security-defensive track.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.