Firewalls: iptables & nftables
Why This Matters
You have just deployed a web server. It is serving pages beautifully on port 80 and 443. But it is also running MySQL on port 3306, an unprotected Redis on port 6379, and SSH on port 22 -- all exposed to the entire internet. Within hours, bots will find the open ports. Within days, someone will brute-force your SSH or exploit your unprotected Redis.
A firewall is the gatekeeper that decides which network traffic is allowed in, allowed out, and allowed to pass through your machine. On Linux, that gatekeeper lives inside the kernel itself, and it is called Netfilter. Everything in this chapter -- iptables, nftables, ufw, firewalld -- is just a different way to talk to Netfilter.
Try This Right Now
# Check if iptables rules exist
sudo iptables -L -n -v
# Check if nftables rules exist
sudo nft list ruleset
# Check if ufw is active (Ubuntu/Debian)
sudo ufw status
# Check if firewalld is active (RHEL/Fedora)
sudo firewall-cmd --state
At least one of these should show you the current firewall state of your system. If all of them return empty results or "inactive," your machine currently has no firewall rules -- all traffic is allowed.
The Netfilter Framework
Netfilter is the packet-filtering framework built into the Linux kernel. It provides hooks at various points in the networking stack where code can inspect and manipulate packets.
Incoming Packet
|
v
+---------------+
| PREROUTING | (NAT, mangle)
+-------+-------+
|
+----Is it for us?----+
| |
v v
+-------+-------+ +-------+-------+
| INPUT | | FORWARD |
| (filter) | | (filter) |
+-------+-------+ +-------+-------+
| |
v v
Local Process +-----+------+
| | POSTROUTING|
v | (NAT) |
+-------+-------+ +-----+------+
| OUTPUT | |
| (filter, NAT) | v
+-------+-------+ Outgoing Packet
|
v
+-------+-------+
| POSTROUTING |
| (NAT) |
+-------+-------+
|
v
Outgoing Packet
The five hooks (also called chains in iptables) are:
| Chain | When it fires |
|---|---|
| PREROUTING | As soon as a packet arrives, before routing decision |
| INPUT | Packet is destined for this machine |
| FORWARD | Packet is passing through this machine to another |
| OUTPUT | Packet originates from this machine |
| POSTROUTING | Just before a packet leaves this machine |
iptables: The Classic Firewall
iptables has been the standard Linux firewall tool since 2001. Even though nftables
is its successor, iptables remains widely deployed and its concepts are essential
knowledge.
Tables and Chains
iptables organizes rules into tables, each containing chains:
+------------------------------------------------------------------+
| Table | Purpose | Chains |
|-------------|--------------------------|-------------------------|
| filter | Packet filtering | INPUT, FORWARD, OUTPUT |
| | (default table) | |
| nat | Network Address | PREROUTING, OUTPUT, |
| | Translation | POSTROUTING |
| mangle | Packet alteration | All five chains |
| raw | Connection tracking | PREROUTING, OUTPUT |
| | exemptions | |
+------------------------------------------------------------------+
Most of the time you will work with the filter table (the default) and occasionally the nat table.
Rule Anatomy
Every iptables rule has this structure:
iptables -t <table> -A <chain> <match-conditions> -j <target>
Where:
-t <table>: Which table (default: filter)-A <chain>: Append to which chain- Match conditions: What to match (source, destination, port, protocol, etc.)
-j <target>: What to do with matching packets
Common targets:
| Target | Action |
|---|---|
| ACCEPT | Allow the packet through |
| DROP | Silently discard the packet (sender gets no response) |
| REJECT | Discard and send an error back (connection refused) |
| LOG | Log the packet, then continue processing the next rule |
| MASQUERADE | Replace source IP with the outgoing interface's IP (NAT) |
Viewing Current Rules
# List all rules in the filter table with line numbers
sudo iptables -L -n -v --line-numbers
# List rules in the nat table
sudo iptables -t nat -L -n -v
# List rules in the raw iptables format (best for scripts)
sudo iptables-save
The -n flag prevents DNS lookups (much faster), and -v shows packet/byte counters.
Essential iptables Examples
Allow established connections (stateful firewall)
This is almost always the first rule you should add:
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
This allows return traffic for connections your machine initiated, and related traffic like ICMP error messages.
Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
Allow HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
Allow loopback traffic
sudo iptables -A INPUT -i lo -j ACCEPT
Allow ping (ICMP)
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
Allow SSH only from a specific network
sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
Drop everything else (default deny)
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
Safety Warning: Setting the INPUT policy to DROP before adding an ACCEPT rule for SSH will immediately lock you out of a remote server. Always add your allow rules first, then set the default policy.
Think About It: Why do we set OUTPUT policy to ACCEPT instead of DROP? Think about what would break if outgoing traffic were blocked by default.
If you set OUTPUT to DROP, your server could not make DNS queries, download package updates, reach NTP servers, or initiate any outgoing connection. While a DROP OUTPUT policy is more secure, it requires carefully whitelisting every outbound connection your server needs, which is difficult to maintain.
A Complete iptables Setup for a Web Server
#!/bin/bash
# Flush existing rules
sudo iptables -F
sudo iptables -t nat -F
# Default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow established and related connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (from management network only)
sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT
# Allow HTTP and HTTPS from anywhere
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow ping
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log dropped packets (optional, useful for debugging)
sudo iptables -A INPUT -j LOG --log-prefix "iptables-dropped: " --log-level 4
Inserting, Deleting, and Replacing Rules
# Insert a rule at position 1 (top of chain)
sudo iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT
# Delete a rule by specification
sudo iptables -D INPUT -p tcp --dport 8080 -j ACCEPT
# Delete a rule by line number
sudo iptables -D INPUT 3
# Replace a rule at position 2
sudo iptables -R INPUT 2 -p tcp --dport 8443 -j ACCEPT
Saving and Restoring Rules
iptables rules are stored in kernel memory and are lost on reboot. You must save them explicitly.
# Save current rules to a file
sudo iptables-save > /etc/iptables/rules.v4
# Restore rules from a file
sudo iptables-restore < /etc/iptables/rules.v4
Distro Note:
Debian/Ubuntu: Install
iptables-persistentto auto-load rules on boot:sudo apt install iptables-persistentRules are stored in/etc/iptables/rules.v4and/etc/iptables/rules.v6.RHEL/CentOS 7: Use
systemctl enable iptablesand save withsudo service iptables save. Rules go to/etc/sysconfig/iptables.RHEL/CentOS 8+, Fedora: firewalld is the default; if you use raw iptables, disable firewalld first.
nftables: The Modern Successor
nftables replaces iptables, ip6tables, arptables, and ebtables with a single unified
framework. It is the default on Debian 10+, RHEL 8+, and Fedora.
Why nftables?
- Single tool for IPv4, IPv6, ARP, and bridging (no more separate
ip6tables) - Better syntax -- more readable and consistent
- Better performance -- rules are compiled into a virtual machine in the kernel
- Atomic rule updates -- load an entire ruleset at once, not rule by rule
- Built-in sets and maps -- native support for collections of addresses/ports
nft Basics
# List the entire ruleset
sudo nft list ruleset
# List tables
sudo nft list tables
# Flush all rules
sudo nft flush ruleset
nftables Structure
nftables uses a hierarchy: tables contain chains, which contain rules.
ruleset
+-- table inet filter
+-- chain input
| +-- rule: accept established
| +-- rule: accept ssh
| +-- rule: drop
+-- chain forward
+-- chain output
The inet family handles both IPv4 and IPv6 simultaneously.
Creating Rules with nft
# Create a table
sudo nft add table inet filter
# Create a chain (base chain attached to a hook)
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
# Allow loopback
sudo nft add rule inet filter input iif lo accept
# Allow established connections
sudo nft add rule inet filter input ct state established,related accept
# Allow SSH
sudo nft add rule inet filter input tcp dport 22 accept
# Allow HTTP and HTTPS
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
# Allow ping
sudo nft add rule inet filter input icmp type echo-request accept
sudo nft add rule inet filter input icmpv6 type echo-request accept
Notice how nftables uses sets ({ 80, 443 }) natively -- no need for multiport
match extensions like in iptables.
A Complete nftables Configuration File
The cleanest way to use nftables is with a configuration file. Create
/etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow loopback
iif lo accept
# Allow established/related connections
ct state established,related accept
# Drop invalid connections
ct state invalid drop
# Allow ICMP and ICMPv6
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Allow SSH from management network
ip saddr 10.0.0.0/24 tcp dport 22 accept
# Allow HTTP and HTTPS
tcp dport { 80, 443 } accept
# Log everything else
log prefix "nftables-dropped: " counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Load it:
sudo nft -f /etc/nftables.conf
# Enable nftables service to load on boot
sudo systemctl enable nftables
Named Sets
One of the most powerful nftables features is named sets:
table inet filter {
set allowed_ssh {
type ipv4_addr
elements = { 10.0.0.5, 10.0.0.10, 192.168.1.0/24 }
}
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ip saddr @allowed_ssh tcp dport 22 accept
tcp dport { 80, 443 } accept
}
}
You can dynamically add and remove elements from a set:
# Add an address to the set
sudo nft add element inet filter allowed_ssh { 10.0.0.20 }
# Remove an address
sudo nft delete element inet filter allowed_ssh { 10.0.0.20 }
Think About It: You are migrating from iptables to nftables. You have 200 iptables rules across three scripts. Is there a way to translate them automatically?
Yes! The iptables-translate command converts iptables rules to nftables syntax:
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# Output: nft add rule ip filter INPUT tcp dport 22 counter accept
And iptables-restore-translate converts an entire iptables-save dump:
iptables-save | iptables-restore-translate
ufw: Uncomplicated Firewall (Ubuntu/Debian)
ufw is a user-friendly frontend to iptables/nftables. It is the default firewall
management tool on Ubuntu.
# Enable ufw
sudo ufw enable
# Check status
sudo ufw status verbose
# Allow SSH (always do this BEFORE enabling!)
sudo ufw allow ssh
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow from a specific network
sudo ufw allow from 10.0.0.0/24 to any port 22
# Deny a specific port
sudo ufw deny 3306/tcp
# Delete a rule
sudo ufw delete allow 80/tcp
# Reset to defaults
sudo ufw reset
# Show numbered rules for deletion
sudo ufw status numbered
sudo ufw delete 3
Safety Warning: Always
sudo ufw allow ssh(orsudo ufw allow 22/tcp) before runningsudo ufw enableon a remote server. Enabling ufw without an SSH rule will lock you out immediately.
firewalld: Zone-Based Firewall (RHEL/Fedora)
firewalld is the default on RHEL, CentOS, and Fedora. It uses the concept of zones
to group interfaces and apply different trust levels.
+--------------------------------------------------------------+
| Zone | Default behavior | Typical use |
|----------------|--------------------|-----------------------|
| drop | Drop all incoming | Maximum restriction |
| block | Reject all incoming| Similar to drop |
| public | Reject, allow SSH | Default zone |
| external | Masquerade, SSH | External-facing NAT |
| dmz | Allow SSH only | DMZ servers |
| work | Allow SSH, some | Work network |
| home | Allow more | Home network |
| internal | Allow more | Internal LAN |
| trusted | Accept all | Maximum trust |
+--------------------------------------------------------------+
firewalld Commands
# Check state
sudo firewall-cmd --state
# Show default zone
sudo firewall-cmd --get-default-zone
# List all zones and their settings
sudo firewall-cmd --list-all-zones
# List current zone rules
sudo firewall-cmd --list-all
# Add a service (temporary, until reload)
sudo firewall-cmd --add-service=http
# Add a service permanently
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
# Add a specific port
sudo firewall-cmd --add-port=8080/tcp --permanent
# Allow from a specific source
sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port port="22" protocol="tcp" accept' --permanent
# Reload to apply permanent changes
sudo firewall-cmd --reload
# Remove a service
sudo firewall-cmd --remove-service=http --permanent
sudo firewall-cmd --reload
Distro Note: On RHEL/Fedora systems, avoid using raw iptables/nftables commands when firewalld is running -- they will conflict. Either use firewalld exclusively or disable it and manage rules directly.
Hands-On: Build a Firewall for an SSH-Only Server
Let's build a lockdown firewall that only allows SSH. We will do this three ways.
Method 1: iptables
# Flush
sudo iptables -F
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow established
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Default deny
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Verify
sudo iptables -L -n -v
Method 2: nftables
sudo nft flush 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 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 tcp dport 22 accept
# Verify
sudo nft list ruleset
Method 3: ufw
sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable
# Verify
sudo ufw status verbose
All three produce effectively the same result: only SSH on port 22 is allowed in, everything else is dropped.
NAT with iptables/nftables
Network Address Translation allows machines on a private network to reach the internet through a Linux gateway.
Source NAT (Masquerade) with iptables
# Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1
# Masquerade outgoing traffic on eth0
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# Allow forwarding from internal network
sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Port Forwarding (DNAT) with iptables
Forward incoming traffic on port 8080 to an internal server:
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.50:80
sudo iptables -A FORWARD -p tcp -d 10.0.0.50 --dport 80 -j ACCEPT
NAT with nftables
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
tcp dport 8080 dnat to 10.0.0.50:80
}
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth0" masquerade
}
}
Debug This
Scenario: You set up a firewall on your web server. Users report that the website is not loading, but you can SSH in just fine. You check:
$ sudo iptables -L -n --line-numbers
Chain INPUT (policy DROP)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate ESTABLISHED,RELATED
2 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
3 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
$ curl -I http://localhost
HTTP/1.1 200 OK
The web server is running (curl from localhost works), and HTTPS (443) is allowed. But users still cannot load the site via HTTP. What is wrong?
Diagnosis: Port 80 (HTTP) is not in the rules. Many users will type http:// or
their browser will try HTTP first. There is a rule for port 443 but not for port 80.
Fix:
sudo iptables -I INPUT 3 -p tcp --dport 80 -j ACCEPT
Or if the web server should redirect HTTP to HTTPS, you still need to allow port 80 so the redirect can happen:
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
What Just Happened?
+-------------------------------------------------------------------+
| Chapter 34 Recap |
+-------------------------------------------------------------------+
| |
| * Netfilter is the kernel framework. iptables and nftables are |
| userspace tools that configure it. |
| |
| * iptables uses tables (filter, nat, mangle) and chains |
| (INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING). |
| |
| * Rules match packets and jump to targets: ACCEPT, DROP, |
| REJECT, LOG, MASQUERADE. |
| |
| * nftables is the modern replacement. It offers a cleaner |
| syntax, native sets, atomic updates, and unified IPv4/IPv6. |
| |
| * ufw (Ubuntu) and firewalld (RHEL/Fedora) are user-friendly |
| frontends -- they still use Netfilter under the hood. |
| |
| * ALWAYS add SSH allow rules before setting default DROP policy. |
| |
| * Save your rules! iptables rules vanish on reboot unless |
| you explicitly save them. |
| |
+-------------------------------------------------------------------+
Try This
-
Lockdown exercise: Starting from a machine with no firewall rules, build a firewall that allows only SSH from your network and ping. Test by scanning yourself with
nmapfrom another machine. -
iptables to nftables: Write five iptables rules, save them with
iptables-save, and then translate them withiptables-restore-translate. Compare the two syntaxes. -
ufw practice: Using ufw, allow HTTP, HTTPS, and SSH. Then add a rule to deny all traffic from a specific IP address. Verify with
sudo ufw status numbered. -
Rate limiting: Use iptables to limit SSH connections to 3 per minute per source IP (hint: use the
limitorrecentmatch module). Test by trying to connect rapidly. -
Bonus challenge: Set up a Linux machine as a NAT gateway. Configure two VMs: one as the gateway with two interfaces (internal and external), and one as an internal client. The client should be able to reach the internet through the gateway using masquerade NAT. Verify with
curlfrom the internal client.