Firewall and iptables/nftables¶
A firewall controls which network traffic is allowed in and out of your system. On a Linux server facing the internet, a properly configured firewall is the first line of defense - it drops malicious traffic before it reaches your applications. Linux implements firewalling through the Netfilter framework in the kernel, which you interact with through command-line tools like iptables, nftables, ufw, and firewalld.
The Netfilter Framework¶
Netfilter is a set of hooks inside the Linux kernel's networking stack. Every network packet that enters, leaves, or passes through the system hits these hooks, where registered rules decide the packet's fate.
The five Netfilter hooks correspond to different points in the packet's journey:
| Hook | When It Fires |
|---|---|
PREROUTING |
Immediately after a packet arrives on a network interface, before routing |
INPUT |
After routing, if the packet is destined for the local system |
FORWARD |
After routing, if the packet is destined for another system (router/gateway) |
OUTPUT |
For packets generated by local processes, before routing |
POSTROUTING |
Just before a packet leaves a network interface, after routing |
You don't interact with Netfilter directly. Instead, you use one of the front-end tools: iptables (legacy), nftables (modern), or a high-level abstraction like ufw or firewalld.
iptables¶
iptables has been the standard Linux firewall tool for over two decades. While nftables is its replacement, iptables is still widely deployed and essential to understand.
Tables and Chains¶
iptables organizes rules into tables, each containing chains:
| Table | Purpose | Default Chains |
|---|---|---|
filter |
Packet filtering (allow/drop) - the default table | INPUT, FORWARD, OUTPUT |
nat |
Network address translation | PREROUTING, OUTPUT, POSTROUTING |
mangle |
Packet header modification | All five hooks |
raw |
Exemptions from connection tracking | PREROUTING, OUTPUT |
When you run iptables without specifying a table, it operates on the filter table.
Rule Syntax¶
# Basic format: iptables -t TABLE -A CHAIN [matches] -j TARGET
# Allow incoming SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow incoming HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow established and related connections (stateful inspection)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback traffic
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow ICMP (ping)
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Set default policy to DROP
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
| Flag | Meaning |
|---|---|
-A |
Append rule to chain |
-I |
Insert rule at position (e.g., -I INPUT 1 inserts at top) |
-D |
Delete a rule |
-P |
Set default policy for a chain |
-p |
Protocol (tcp, udp, icmp) |
--dport |
Destination port |
--sport |
Source port |
-s |
Source address |
-d |
Destination address |
-i |
Input interface |
-o |
Output interface |
-j |
Target (what to do with the packet) |
-m |
Match extension module |
Match Extensions¶
# Match multiple ports at once
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT
# Rate limit connections (prevent brute force)
sudo iptables -A INPUT -p tcp --dport 22 -m limit --limit 3/minute --limit-burst 5 -j ACCEPT
# Match by connection state
sudo iptables -A INPUT -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# Match by source IP range
sudo iptables -A INPUT -m iprange --src-range 10.0.1.1-10.0.1.50 -j ACCEPT
Targets¶
| Target | Effect |
|---|---|
ACCEPT |
Allow the packet through |
DROP |
Silently discard the packet |
REJECT |
Discard and send an error back to the sender |
LOG |
Log the packet to the kernel log, then continue processing |
MASQUERADE |
Rewrite source address (NAT, used in POSTROUTING) |
DNAT |
Rewrite destination address (port forwarding) |
SNAT |
Rewrite source address to a fixed IP |
DROP vs REJECT: DROP gives the sender no feedback (the connection times out), making port scanning slower. REJECT sends an immediate response. For internet-facing servers, DROP is generally preferred.
Listing, Deleting, and Saving Rules¶
# List all rules with line numbers, packet counts, and numeric addresses
sudo iptables -L -v -n --line-numbers
# List rules for a specific chain
sudo iptables -L INPUT -v -n --line-numbers
# Delete a rule by line number
sudo iptables -D INPUT 3
# Delete a rule by specification
sudo iptables -D INPUT -p tcp --dport 8080 -j ACCEPT
# Flush all rules (remove everything)
sudo iptables -F
# Save rules (Debian/Ubuntu)
sudo iptables-save > /etc/iptables/rules.v4
# Restore rules
sudo iptables-restore < /etc/iptables/rules.v4
Rule ordering matters
iptables processes rules top to bottom. The first matching rule wins. If you put a DROP rule before an ACCEPT rule for the same traffic, the traffic is dropped. Always put more specific rules above more general ones, and stateful rules (ESTABLISHED,RELATED) near the top.
nftables¶
nftables is the modern replacement for iptables. It provides a cleaner syntax, better performance, and combines IPv4, IPv6, ARP, and bridge filtering into a single framework. Most distributions now ship nftables as the default, with iptables commands translated through a compatibility layer (iptables-nft).
Core Concepts¶
In nftables, you create your own tables and chains rather than using predefined ones:
# Create a table
sudo nft add table ip filter
# Create a chain with a hook and default policy
sudo nft add chain ip filter input { type filter hook input priority 0 \; policy drop \; }
# Add rules to the chain
sudo nft add rule ip filter input iif lo accept
sudo nft add rule ip filter input ct state established,related accept
sudo nft add rule ip filter input tcp dport 22 accept
sudo nft add rule ip filter input tcp dport { 80, 443 } accept
sudo nft add rule ip filter input icmp type echo-request accept
nftables vs iptables Syntax¶
| iptables | nftables |
|---|---|
iptables -A INPUT |
nft add rule ip filter input |
-p tcp --dport 22 |
tcp dport 22 |
-m multiport --dports 80,443 |
tcp dport { 80, 443 } |
-m conntrack --ctstate ESTABLISHED |
ct state established |
-j ACCEPT |
accept |
-j DROP |
drop |
iptables -L |
nft list ruleset |
Sets and Maps¶
nftables has native support for sets, eliminating the need for ipset:
# Create a named set of allowed IPs
sudo nft add set ip filter trusted { type ipv4_addr \; }
sudo nft add element ip filter trusted { 10.0.1.50, 10.0.1.51, 192.168.1.0/24 }
# Use the set in a rule
sudo nft add rule ip filter input ip saddr @trusted accept
# Dynamically add to a set (e.g., for rate limiting)
sudo nft add set ip filter ratelimit { type ipv4_addr \; flags dynamic,timeout \; timeout 5m \; }
Configuration File¶
nftables rules are typically stored in /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
icmp type echo-request accept
# Log dropped packets (optional)
log prefix "nftables-drop: " counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# Load the configuration
sudo nft -f /etc/nftables.conf
# Enable at boot
sudo systemctl enable nftables
# List the full ruleset
sudo nft list ruleset
Migrating from iptables¶
# Translate iptables rules to nftables format
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# Output: nft add rule ip filter INPUT tcp dport 22 counter accept
# Export entire iptables ruleset as nftables
iptables-save | iptables-restore-translate
UFW (Uncomplicated Firewall)¶
ufw is the default firewall front-end on Ubuntu and Debian. It generates iptables/nftables rules behind the scenes but provides a much simpler interface.
Basic Operations¶
# Enable the firewall
sudo ufw enable
# Disable the firewall
sudo ufw disable
# Check status and rules
sudo ufw status
sudo ufw status verbose
sudo ufw status numbered
# Reset to defaults (removes all rules)
sudo ufw reset
Adding Rules¶
# Allow by service name
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
# Allow by port number
sudo ufw allow 8080/tcp
sudo ufw allow 53/udp
# Allow from a specific IP
sudo ufw allow from 10.0.1.50
# Allow from a subnet to a specific port
sudo ufw allow from 10.0.1.0/24 to any port 5432 proto tcp
# Deny a specific port
sudo ufw deny 3306/tcp
# Rate limit SSH (allows 6 connections per 30 seconds per IP)
sudo ufw limit ssh
# Delete a rule by number
sudo ufw status numbered
sudo ufw delete 3
# Delete a rule by specification
sudo ufw delete allow 8080/tcp
Application Profiles¶
ufw reads application profiles from /etc/ufw/applications.d/:
# List available profiles
sudo ufw app list
# Show profile details
sudo ufw app info "Nginx Full"
# Allow by profile
sudo ufw allow "Nginx Full"
Logging¶
# Enable logging (default: low)
sudo ufw logging on
# Set log level (off, low, medium, high, full)
sudo ufw logging medium
# Logs go to /var/log/ufw.log
Default deny is automatic
When you enable ufw, it sets the default incoming policy to deny and outgoing to allow. You only need to add rules for traffic you want to accept. This makes ufw inherently safe - enable it and add your allow rules.
firewalld¶
firewalld is the default on RHEL, Fedora, CentOS, Rocky, and AlmaLinux. Instead of chains and rules, it organizes traffic into zones.
Zones¶
A zone defines the trust level for a network connection. Common zones:
| Zone | Default Policy | Use Case |
|---|---|---|
drop |
Drop all incoming | Maximum restriction |
block |
Reject all incoming | Like drop, but sends ICMP rejection |
public |
Deny incoming (selective allow) | Internet-facing servers (default) |
external |
Deny incoming, masquerade outgoing | NAT gateways |
internal |
Allow some services | Internal network interfaces |
trusted |
Allow all | Fully trusted networks |
Managing Services and Ports¶
# Check the default zone
firewall-cmd --get-default-zone
# List active zones and their interfaces
firewall-cmd --get-active-zones
# List what's allowed in the public zone
firewall-cmd --zone=public --list-all
# Allow a service (runtime only - lost on restart)
sudo firewall-cmd --zone=public --add-service=http
# Allow a service permanently
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
# Remove a service
sudo firewall-cmd --zone=public --remove-service=http --permanent
sudo firewall-cmd --reload
# List all available services
firewall-cmd --get-services
Runtime vs Permanent¶
firewalld has two configuration layers:
- Runtime - active immediately but lost on restart
- Permanent - saved to disk but requires
--reloadto activate
Always add --permanent and then --reload, or add the runtime rule first for testing and then add --permanent once confirmed.
Rich Rules¶
For complex rules that services and ports can't express:
# Allow SSH from a specific subnet
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.1.0/24" service name="ssh" accept' --permanent
# Rate limit SSH connections
sudo firewall-cmd --zone=public --add-rich-rule='rule service name="ssh" limit value="3/m" accept' --permanent
# Log and drop traffic from a specific IP
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.50" log prefix="blocked: " drop' --permanent
sudo firewall-cmd --reload
NAT and Port Forwarding¶
Network Address Translation lets you rewrite packet addresses - essential for sharing a single public IP across multiple servers, or forwarding traffic to internal hosts.
Masquerading (SNAT)¶
Masquerading rewrites the source address of outgoing packets, allowing internal hosts to reach the internet through a gateway:
# iptables
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# nftables
sudo nft add rule ip nat postrouting oifname "eth0" masquerade
# firewalld
sudo firewall-cmd --zone=external --add-masquerade --permanent
You also need to enable IP forwarding:
# Enable immediately
sudo sysctl -w net.ipv4.ip_forward=1
# Persist across reboots
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-forwarding.conf
sudo sysctl --system
Port Forwarding (DNAT)¶
Forward incoming traffic on one port to a different host or port:
# iptables: forward port 8080 to internal server 10.0.1.50:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.1.50:80
sudo iptables -A FORWARD -p tcp -d 10.0.1.50 --dport 80 -j ACCEPT
# firewalld: forward port 8080 to internal server
sudo firewall-cmd --zone=public --add-forward-port=port=8080:proto=tcp:toport=80:toaddr=10.0.1.50 --permanent
sudo firewall-cmd --reload
Best Practices¶
Default deny: Start by dropping everything, then explicitly allow only what's needed. This is safer than trying to block known-bad traffic.
Stateful rules first: Place ESTABLISHED,RELATED rules near the top. Most packets belong to existing connections, so matching them early improves performance and ensures responses to legitimate outgoing requests always get through.
Rule ordering: More specific rules go before more general ones. A broad DROP rule placed too early hides more specific ACCEPT rules below it.
Rate limiting: Protect SSH and other authentication services from brute force:
# iptables: limit new SSH connections
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 3/minute --limit-burst 5 -j ACCEPT
# ufw: built-in rate limiting
sudo ufw limit ssh
Persistence: iptables rules are lost on reboot unless saved. Use iptables-save/iptables-restore, or install iptables-persistent on Debian/Ubuntu. nftables and firewalld handle persistence natively.
Testing: Before applying restrictive rules on a remote server, set a cron job to restore permissive rules in case you lock yourself out:
# Safety net: restore open rules in 5 minutes
echo "iptables -F && iptables -P INPUT ACCEPT" | at now + 5 minutes
Don't lock yourself out
The most common firewall mistake is blocking SSH before adding an allow rule for it. If you're configuring a firewall over SSH, always ensure your SSH access rule is in place and tested before setting a default deny policy. Keep a console session or out-of-band access available as a backup.
Troubleshooting¶
# iptables: list rules with packet counters
sudo iptables -L -v -n --line-numbers
# nftables: show full ruleset
sudo nft list ruleset
# ufw: verbose status
sudo ufw status verbose
# firewalld: show everything in a zone
sudo firewall-cmd --zone=public --list-all
# Test if a port is reachable from another machine
nc -zv server.example.com 80
# Check which process is listening on a port
sudo ss -tlnp | grep :80
# Watch for dropped packets in real time (with LOG target)
sudo journalctl -f | grep "nftables-drop"
When a connection fails, check in order:
1. Is the service running and listening? (ss -tlnp)
2. Is the firewall allowing the port? (iptables -L -n or nft list ruleset)
3. Is there a network-level issue? (ping, traceroute)
Interactive Quizzes¶
Further Reading¶
- Netfilter Project - home of iptables, nftables, and related tools
- nftables Wiki - official nftables documentation and examples
- iptables man page - iptables command reference
- UFW Documentation - Ubuntu's guide to the Uncomplicated Firewall
- firewalld Documentation - official firewalld configuration guide
- Arch Wiki: nftables - comprehensive practical reference
Previous: Cron and Scheduled Tasks | Next: Log Management | Back to Index