Linux iptables: Firewall Configuration

• iptables operates on a tables-chains-rules hierarchy where packets traverse specific chains (INPUT, OUTPUT, FORWARD) within tables (filter, nat, mangle, raw) and are matched against rules in order...

Key Insights

• iptables operates on a tables-chains-rules hierarchy where packets traverse specific chains (INPUT, OUTPUT, FORWARD) within tables (filter, nat, mangle, raw) and are matched against rules in order until a target action is reached.

• Always set a default DROP policy and explicitly whitelist required services rather than blacklisting threats—this “deny by default” approach dramatically reduces your attack surface and prevents configuration oversights from exposing services.

• Rules are not persistent by default and will vanish on reboot; use iptables-save/iptables-restore with systemd integration or distribution-specific tools to ensure your firewall configuration survives system restarts.

Introduction to iptables

iptables is Linux’s standard userspace utility for configuring the kernel’s netfilter packet filtering framework. It’s been the de facto firewall solution for Linux systems for over two decades, providing stateful packet inspection, network address translation, and packet manipulation capabilities. While nftables is positioned as its successor, iptables remains widely deployed and understanding it is essential for managing production Linux systems.

The architecture consists of tables that contain chains, which in turn contain rules. When a packet enters the system, it traverses specific chains based on its direction and destination. Each rule in a chain specifies match criteria (source IP, destination port, protocol, etc.) and a target action (ACCEPT, DROP, REJECT, LOG, etc.). The first matching rule determines the packet’s fate.

Check your current firewall state with:

sudo iptables -L -v -n

The -L lists rules, -v provides verbose output including packet counters, and -n shows numeric addresses instead of resolving hostnames. On a fresh system, you’ll see empty chains with ACCEPT policies—meaning all traffic is allowed by default.

Understanding Tables and Chains

iptables organizes rules into four distinct tables, each serving a specific purpose:

filter: The default table for packet filtering decisions. This is where you allow or block traffic. Contains INPUT (packets destined for local processes), OUTPUT (locally-generated packets), and FORWARD (packets routed through the system) chains.

nat: Handles network address translation for routing packets between networks. Contains PREROUTING (alter packets as they arrive), POSTROUTING (alter packets before they leave), and OUTPUT chains. Essential for routers and systems performing masquerading.

mangle: Used for specialized packet alteration like modifying TTL or TOS values. Contains all five chains: PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING.

raw: Configures exemptions from connection tracking. Primarily contains PREROUTING and OUTPUT chains. Rarely used in typical configurations.

Packet flow through these tables follows a specific order. For incoming packets destined for local processes: raw PREROUTING → mangle PREROUTING → nat PREROUTING → mangle INPUT → filter INPUT. For locally-generated outbound packets: raw OUTPUT → mangle OUTPUT → nat OUTPUT → filter OUTPUT → mangle POSTROUTING → nat POSTROUTING.

View a specific table with:

sudo iptables -t nat -L -v -n
sudo iptables -t mangle -L -v -n

When you omit -t, the filter table is assumed since it’s used most frequently.

Basic Rule Syntax and Operations

The fundamental iptables command structure follows this pattern:

iptables [-t table] command chain match-criteria -j target

Common commands include -A (append rule to end of chain), -I (insert rule at specific position), -D (delete rule), and -P (set default policy).

Allow SSH access on port 22:

sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
sudo iptables -A OUTPUT -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED -j ACCEPT

This creates stateful rules that permit new SSH connections inbound and allow the corresponding reply traffic outbound.

Block traffic from a specific IP address:

sudo iptables -A INPUT -s 192.168.1.100 -j DROP

Allow established and related connections (critical for stateful filtering):

sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT

These rules should appear early in your chains since they’ll match most traffic on an active system, improving performance.

Set default policies to DROP (deny by default):

sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

Warning: Be extremely careful when setting INPUT policy to DROP. Ensure you have rules allowing SSH or console access before doing this, or you’ll lock yourself out of remote systems.

Common Firewall Configurations

Here’s a complete ruleset for a typical web server running a LAMP stack:

#!/bin/bash
# Flush existing rules
iptables -F
iptables -X

# Default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow SSH (consider restricting to specific IPs in production)
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

# Allow HTTP and HTTPS
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT

# Allow ping (ICMP echo requests)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Log dropped packets (optional, can generate significant logs)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables_INPUT_denied: " --log-level 7

# Drop everything else (implicit due to policy, but explicit DROP can be added)

For a database server that should only accept connections from application servers:

# Allow MySQL from app servers only
iptables -A INPUT -p tcp -s 10.0.1.0/24 --dport 3306 -m conntrack --ctstate NEW -j ACCEPT

# Allow PostgreSQL from specific hosts
iptables -A INPUT -p tcp -s 10.0.1.10 --dport 5432 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.1.11 --dport 5432 -m conntrack --ctstate NEW -j ACCEPT

Implement rate limiting to mitigate basic DDoS attempts:

# Limit SSH connections to 3 per minute from same IP
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 -j DROP

# Limit HTTP connections to prevent slowloris-style attacks
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -m limit --limit 100/minute --limit-burst 200 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j DROP

Persistence and Management

iptables rules exist only in memory and disappear on reboot. Save your configuration:

sudo iptables-save > /etc/iptables/rules.v4

Restore rules from file:

sudo iptables-restore < /etc/iptables/rules.v4

On Debian/Ubuntu systems, install iptables-persistent:

sudo apt-get install iptables-persistent

This package automatically saves rules to /etc/iptables/rules.v4 and /etc/iptables/rules.v6 and restores them on boot via systemd.

For RHEL/CentOS systems, use:

sudo service iptables save

This writes rules to /etc/sysconfig/iptables.

Create a systemd service for custom rule management:

# /etc/systemd/system/iptables-custom.service
[Unit]
Description=Custom iptables rules
Before=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/firewall-rules.sh
ExecStop=/usr/sbin/iptables -F
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Store your rules in /usr/local/bin/firewall-rules.sh, make it executable, then enable the service:

sudo systemctl enable iptables-custom.service
sudo systemctl start iptables-custom.service

Troubleshooting and Best Practices

Enable logging for dropped packets to diagnose connection issues:

# Add before final DROP policy or rule
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables_denied: " --log-level 4

Check logs with:

sudo tail -f /var/log/syslog | grep iptables_denied
# or on systems using journald
sudo journalctl -kf | grep iptables_denied

When testing rules on a remote system, use this safety pattern:

# Apply rules and schedule auto-flush in 5 minutes
sudo iptables [your rules]
sudo at now + 5 minutes <<< "iptables -F; iptables -P INPUT ACCEPT"

# If rules work correctly, cancel the at job
sudo atq  # note the job number
sudo atrm [job-number]

This creates a dead man’s switch—if your rules lock you out, they’ll automatically flush after 5 minutes.

Safely flush all rules and reset to permissive state:

sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X

Best practices:

  • Always set default DROP policies and explicitly whitelist services
  • Place frequently-matched rules (like ESTABLISHED,RELATED) early in chains
  • Use specific match criteria rather than broad rules
  • Document your rules with comments in script files
  • Test rule changes on non-production systems first
  • Monitor logs for legitimate traffic being blocked
  • Regularly audit rules and remove obsolete entries
  • Consider migrating to nftables for new deployments—it offers better performance, simpler syntax, and atomic rule updates

While iptables remains battle-tested and ubiquitous, nftables provides a more modern framework. However, iptables knowledge transfers directly to nftables concepts, making time invested in learning iptables valuable regardless of future migration plans.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.