What is a Firewall?#
A firewall inspects network traffic and decides what to allow or block based on rules you define. On Linux, the firewall operates in the kernel’s netfilter framework — iptables and nftables are the tools you use to configure it.
Every packet entering, leaving, or passing through your machine is evaluated against your rules. No rule match means the default policy applies.
iptables vs nftables#
| iptables | nftables | |
|---|---|---|
| Status | Legacy (still widely used) | Modern replacement |
| Syntax | Separate commands per rule | Structured, scriptable |
| Performance | Slower with many rules | Better with large rulesets |
| Default on | Older systems, Ubuntu < 22.04 | Fedora, RHEL 9+, Debian 11+ |
Both do the same job. iptables is still everywhere in documentation and production, so you need to know it. nftables is the future. This post covers both.
iptables Fundamentals#
Tables and chains#
iptables organizes rules into tables, each containing chains:
| Table | Purpose | Common Chains |
|---|---|---|
filter | Accept/drop traffic (default) | INPUT, OUTPUT, FORWARD |
nat | Rewrite addresses/ports | PREROUTING, POSTROUTING, OUTPUT |
mangle | Modify packet headers | All |
The filter table is what you use 90% of the time:
| Chain | Handles traffic… |
|---|---|
INPUT | Coming into this machine |
OUTPUT | Going out from this machine |
FORWARD | Passing through this machine (routing) |
How rules are evaluated#
Packets hit chains top-to-bottom. The first matching rule wins:
Packet arrives → INPUT chain
Rule 1: Allow SSH → no match, continue
Rule 2: Allow HTTP → MATCH → accept
Rule 3: Drop all → never reached for this packetIf no rule matches, the chain’s default policy applies.
Viewing current rules#
# Show all rules (filter table)
sudo iptables -L -n -v
# Show with line numbers
sudo iptables -L -n --line-numbers
# Show NAT rules
sudo iptables -t nat -L -n| Flag | Meaning |
|---|---|
-L | List rules |
-n | Numeric output (don’t resolve DNS) |
-v | Verbose (show counters) |
--line-numbers | Show rule positions |
Setting the default policy#
# Drop everything by default (whitelist approach — recommended)
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPTWarning: Setting INPUT to DROP without first allowing SSH will lock you out of a remote server. Always add your allow rules first.
Adding rules#
# Allow established/related connections (critical — lets responses come back)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback (localhost)
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow SSH (port 22)
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 ping
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Drop everything else (explicit, even if policy is DROP)
sudo iptables -A INPUT -j DROP| Option | Meaning |
|---|---|
-A INPUT | Append to the INPUT chain |
-p tcp | Match TCP protocol |
--dport 22 | Match destination port 22 |
-j ACCEPT | Jump to ACCEPT (allow the packet) |
-j DROP | Silently discard the packet |
-j REJECT | Discard and send an error back |
-i lo | Match traffic on the loopback interface |
-m conntrack | Use connection tracking module |
--ctstate | Match connection state |
Deleting rules#
# Delete by line number
sudo iptables -D INPUT 3
# Delete by specification (exact match)
sudo iptables -D INPUT -p tcp --dport 80 -j ACCEPT
# Flush all rules in a chain
sudo iptables -F INPUT
# Flush everything
sudo iptables -FInserting rules at a position#
# Insert at position 1 (top of chain)
sudo iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPTAllowing traffic from a specific IP or subnet#
# Allow all traffic from a trusted IP
sudo iptables -A INPUT -s 192.168.1.100 -j ACCEPT
# Allow SSH only from a subnet
sudo iptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 -j ACCEPT
# Block a specific IP
sudo iptables -A INPUT -s 203.0.113.50 -j DROPRate limiting#
# Limit SSH to 3 new connections per minute (prevents brute force)
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 3/min --limit-burst 3 -j ACCEPTnftables Fundamentals#
nftables replaces iptables with a cleaner syntax. Instead of separate commands per rule, you define tables and chains in a structured format.
Viewing current rules#
sudo nft list rulesetCreating a basic firewall#
# Create a table
sudo nft add table inet filter
# Create the input chain with a default drop policy
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
# Create the output chain (allow by default)
sudo nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }
# Allow established connections
sudo nft add rule inet filter input ct state established,related accept
# Allow loopback
sudo nft add rule inet filter input iifname "lo" accept
# Allow SSH
sudo nft add rule inet filter input tcp dport 22 accept
# Allow HTTP/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 acceptnftables advantages#
# Multiple ports in one rule
sudo nft add rule inet filter input tcp dport { 22, 80, 443, 8080 } accept
# IP sets
sudo nft add set inet filter trusted_ips { type ipv4_addr \; }
sudo nft add element inet filter trusted_ips { 192.168.1.10, 192.168.1.20 }
sudo nft add rule inet filter input ip saddr @trusted_ips accept
# Rate limiting
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 3/minute acceptDeleting rules in nftables#
# List rules with handles
sudo nft -a list chain inet filter input
# Delete by handle number
sudo nft delete rule inet filter input handle 7
# Flush a chain
sudo nft flush chain inet filter input
# Delete entire table
sudo nft delete table inet filternftables config file#
Save your rules to a file for persistence:
# Export current rules
sudo nft list ruleset > /etc/nftables.conf
# Load rules from file
sudo nft -f /etc/nftables.confA complete /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iifname "lo" accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
icmp type echo-request accept
}
chain output {
type filter hook output priority 0; policy accept;
}
chain forward {
type filter hook forward priority 0; policy drop;
}
}Enable on boot:
sudo systemctl enable nftablesMaking iptables Rules Persistent#
iptables rules are lost on reboot by default. To persist them:
Debian/Ubuntu#
sudo apt install iptables-persistent
# Save current rules
sudo netfilter-persistent save
# Rules stored in:
# /etc/iptables/rules.v4
# /etc/iptables/rules.v6RHEL/Fedora#
# Save
sudo iptables-save > /etc/sysconfig/iptables
# Restore on boot (handled by iptables service)
sudo systemctl enable iptablesManual save/restore#
# Save to a file
sudo iptables-save > ~/firewall-rules.txt
# Restore from a file
sudo iptables-restore < ~/firewall-rules.txtA Complete Server Firewall#
iptables version#
# Flush existing rules
sudo iptables -F
sudo iptables -X
# Default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow established connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP/HTTPS
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 "DROPPED: " --log-level 4
# Drop everything else
sudo iptables -A INPUT -j DROPChecking logged drops#
sudo journalctl -k | grep DROPPED
# or
dmesg | grep DROPPEDCommon Patterns#
Allow a service only from your network#
# SSH only from local network
sudo iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
# Database only from app servers
sudo iptables -A INPUT -s 10.0.1.0/24 -p tcp --dport 5432 -j ACCEPTBlock outgoing traffic to a host#
sudo iptables -A OUTPUT -d 198.51.100.0/24 -j DROPPort forwarding (NAT)#
# Forward port 8080 to internal server on port 80
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80
sudo iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPTTroubleshooting Firewalls#
Check if a port is blocked#
# From another machine
nc -zv yourserver.com 80
# Locally — check if something is listening
ss -tlnp | grep :80If ss shows a listener but remote nc times out, the firewall is blocking it.
Watch packets hit rules#
# Show packet and byte counters
sudo iptables -L -n -v
# Zero counters, wait, then check again
sudo iptables -Z
# ... send some traffic ...
sudo iptables -L -n -vTemporarily disable the firewall#
# iptables — accept everything
sudo iptables -P INPUT ACCEPT
sudo iptables -F
# nftables
sudo nft flush rulesetDon’t forget to restore your rules after testing.
Best Practices#
- Always allow established/related connections first — without this, outbound connections won’t get responses back
- Use a default DROP policy and explicitly allow what you need (whitelist approach)
- Allow SSH before setting the default policy to DROP on remote servers — otherwise you lock yourself out
- Use connection tracking (
conntrack) rather than manually allowing return traffic - Log dropped packets during initial setup to see what’s being blocked unintentionally
- Keep rules ordered — more specific rules first, broad catches at the end
- Use nftables for new setups — it’s cleaner and better for complex rulesets
- Persist your rules — iptables rules are lost on reboot unless explicitly saved
- Test firewall changes with a timeout on remote servers:
sudo bash -c "sleep 60 && iptables -F" &before applying restrictive rules


