iptables — Firewall Reference

The classic Linux firewall — a userspace interface to the kernel's netfilter framework for filtering, NATting, and manipulating IP packets on any Linux host.

filter  ·  nat  ·  mangle  ·  raw  ·  IPv4 & IPv6  ·  stateful inspection
01 — What is iptables?

iptables is a userspace command-line tool that configures the Linux kernel's netfilter packet-filtering framework. Rules are organised into tableschainsrules. Each incoming, outgoing, or forwarded packet traverses the relevant chains in order; the first matching rule wins.

iptables has been the backbone of Linux firewalling for over two decades — understanding its table–chain–rule model unlocks full control over every packet that crosses your host.

ip6tables is the IPv6 counterpart with an identical syntax. Everything in this document applies to both unless noted.

TablePurposeBuilt-in chains
filterAllow, drop, or reject packets. The default table.INPUT, FORWARD, OUTPUT
natNetwork Address Translation — SNAT, DNAT, masquerade.PREROUTING, OUTPUT, POSTROUTING
mangleModify packet headers (TTL, TOS, mark).PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING
rawBypass connection tracking (NOTRACK). Highest priority.PREROUTING, OUTPUT
securityMandatory access control (SELinux). Rarely touched directly.INPUT, FORWARD, OUTPUT
Packet flow — simplified
Incoming packet │ ▼ [raw PREROUTING] → [mangle PREROUTING] → [nat PREROUTING] ← DNAT here │ ├─ Destined for THIS host ──► [mangle INPUT] → [filter INPUT] → local process │ └─ Destined for ANOTHER host ─► [mangle FORWARD] → [filter FORWARD] │ [mangle POSTROUTING] → [nat POSTROUTING] → wire ▲ Outgoing packet from local process SNAT / MASQUERADE here │ ▼ [raw OUTPUT] → [mangle OUTPUT] → [nat OUTPUT] → [filter OUTPUT] │ └──────────────────────────────────────────────► [POSTROUTING] → wire
02 — Installation & Basics
Install
# Debian / Ubuntu sudo apt install iptables iptables-persistent # RHEL / Fedora (replace nftables-based firewalld) sudo dnf install iptables-services sudo systemctl disable --now firewalld sudo systemctl enable --now iptables # Arch sudo pacman -S iptables
Essential commands
# List rules with line numbers and packet counts sudo iptables -L -v -n --line-numbers # List a specific table sudo iptables -t nat -L -v -n --line-numbers # Flush ALL rules (dangerous — test first!) sudo iptables -F # Flush a specific chain sudo iptables -F INPUT # Zero packet/byte counters sudo iptables -Z # Save rules (Debian/Ubuntu with iptables-persistent) sudo netfilter-persistent save # Save rules (RHEL) sudo service iptables save # Restore rules from file sudo iptables-restore < /etc/iptables/rules.v4
⚠  Always have console / out-of-band access when modifying firewall rules on a remote host. A single wrong DROP rule will lock you out. Use iptables-restore with a timed reset script as a safety net.
03 — Rule Syntax

Every iptables command follows a consistent structure. Understanding each flag makes building rules intuitive.

iptables [-t table] COMMAND chain [matches] -j TARGET # Examples iptables -A INPUT -p tcp --dport 22 -j ACCEPT iptables -I INPUT 1 -s 10.0.0.5 -j DROP iptables -D INPUT -p tcp --dport 80 -j ACCEPT
Commands
CommandMeaning
-A chainAppend rule to end of chain.
-I chain [n]Insert rule at position n (default: 1 = top).
-D chain [n|rule]Delete rule by number or specification.
-R chain nReplace rule at position n.
-L [chain]List rules.
-F [chain]Flush (delete all rules in) chain or table.
-P chain TARGETSet default policy for a built-in chain.
-N chainCreate a new user-defined chain.
-X [chain]Delete a user-defined chain.
-E old newRename a chain.
-Z [chain]Zero packet/byte counters.
Common matches
MatchDescriptionExample
-s <ip/cidr>Source IP or range-s 192.168.1.0/24
-d <ip/cidr>Destination IP or range-d 10.0.0.1
-p <proto>Protocol: tcp, udp, icmp, all-p tcp
--dport <port>Destination port (requires -p)--dport 443
--sport <port>Source port--sport 1024:65535
-i <iface>Incoming interface-i eth0
-o <iface>Outgoing interface-o eth1
-m state --stateConnection tracking state-m state --state ESTABLISHED,RELATED
-m conntrack --ctstateModern conntrack (preferred)-m conntrack --ctstate NEW,ESTABLISHED
-m multiport --dportsMultiple destination ports-m multiport --dports 80,443,8080
-m iprange --src-rangeIP range (not CIDR)--src-range 10.0.0.1-10.0.0.50
-m limit --limitRate limit matches-m limit --limit 5/min
-m recentRecent IP tracking (rate limiting)-m recent --name SSH --update
-m string --stringMatch payload string--string "attack" --algo bm
-m mac --mac-sourceMatch source MAC address--mac-source AA:BB:CC:DD:EE:FF
Targets
TargetEffect
ACCEPTAllow the packet through.
DROPSilently discard the packet. Sender gets no response.
REJECTDiscard and send an ICMP error back to the sender.
LOGLog the packet to the kernel log (dmesg / syslog) and continue.
RETURNStop traversing the current chain; return to the calling chain.
MASQUERADESNAT for dynamic IPs (nat table, POSTROUTING).
SNATRewrite source IP to a fixed address (nat POSTROUTING).
DNATRewrite destination IP / port (nat PREROUTING).
REDIRECTRedirect to a local port (nat PREROUTING).
MARKSet packet mark for routing / QoS (mangle table).
custom chainJump to a user-defined chain.
04 — Stateful Firewall (the Standard Template)

A stateful firewall tracks connection state so you only need to explicitly allow new inbound connections; return traffic for established sessions is permitted automatically. This is the foundation of almost every Linux server firewall.

Standard server ruleset — apply in order
#!/bin/bash # ── FLUSH existing rules ────────────────────────────────────────────── iptables -F iptables -X iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X # ── DEFAULT POLICIES ────────────────────────────────────────────────── # Drop everything, then selectively allow iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # outbound unrestricted (tighten as needed) # ── LOOPBACK ────────────────────────────────────────────────────────── iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # ── ESTABLISHED / RELATED ───────────────────────────────────────────── # Accept return traffic for connections we initiated or allowed in iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # ── ICMP ────────────────────────────────────────────────────────────── # Allow ping (rate-limited to mitigate flood) iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s --limit-burst 5 -j ACCEPT iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT # ── SSH ─────────────────────────────────────────────────────────────── iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT # ── HTTP / HTTPS ────────────────────────────────────────────────────── iptables -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j ACCEPT # ── LOG & DROP everything else ──────────────────────────────────────── iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-INPUT-DROP: " --log-level 4 iptables -A INPUT -j DROP iptables -A FORWARD -j DROP
Save the ruleset
# Debian / Ubuntu sudo netfilter-persistent save # writes /etc/iptables/rules.v4 and rules.v6 # RHEL / CentOS sudo service iptables save # writes /etc/sysconfig/iptables # Manual sudo iptables-save > /etc/iptables/rules.v4
05 — Common Service Rules
SSH
# Allow SSH from anywhere (simplest) iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT # Allow SSH from a specific IP or subnet only iptables -A INPUT -p tcp --dport 22 -s 203.0.113.0/24 -m conntrack --ctstate NEW -j ACCEPT # Rate-limit SSH to 3 new connections per minute (brute-force mitigation) iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --set --name SSH iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT # Custom SSH port iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW -j ACCEPT
HTTP & HTTPS
# Allow HTTP and HTTPS iptables -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j ACCEPT # Allow only HTTPS, block plain HTTP iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j REJECT --reject-with tcp-reset # Rate-limit HTTP new connections (basic DDoS mitigation) iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW \ -m limit --limit 50/s --limit-burst 200 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j DROP
FTP (active & passive)
# Load the conntrack FTP helper (required for stateful FTP) sudo modprobe nf_conntrack_ftp # FTP control channel iptables -A INPUT -p tcp --dport 21 -m conntrack --ctstate NEW -j ACCEPT # Active FTP — server connects back on port 20 iptables -A INPUT -p tcp --dport 20 -m conntrack --ctstate ESTABLISHED -j ACCEPT iptables -A OUTPUT -p tcp --sport 20 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Passive FTP — server opens high port; conntrack tracks it automatically # (requires nf_conntrack_ftp module loaded above) iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # FTPS (FTP over TLS explicit) iptables -A INPUT -p tcp --dport 990 -m conntrack --ctstate NEW -j ACCEPT iptables -A INPUT -p tcp --dport 989 -m conntrack --ctstate NEW -j ACCEPT
SMTP / Mail
# SMTP (plain — outbound relay, often blocked by ISPs) iptables -A INPUT -p tcp --dport 25 -m conntrack --ctstate NEW -j ACCEPT # Submission (authenticated SMTP — preferred for clients) iptables -A INPUT -p tcp --dport 587 -m conntrack --ctstate NEW -j ACCEPT # SMTPS (SMTP over TLS) iptables -A INPUT -p tcp --dport 465 -m conntrack --ctstate NEW -j ACCEPT
IMAP & POP3
# IMAP iptables -A INPUT -p tcp --dport 143 -m conntrack --ctstate NEW -j ACCEPT # IMAPS (IMAP over TLS) iptables -A INPUT -p tcp --dport 993 -m conntrack --ctstate NEW -j ACCEPT # POP3 iptables -A INPUT -p tcp --dport 110 -m conntrack --ctstate NEW -j ACCEPT # POP3S (POP3 over TLS) iptables -A INPUT -p tcp --dport 995 -m conntrack --ctstate NEW -j ACCEPT
DNS
# DNS server (accepts queries from clients) iptables -A INPUT -p udp --dport 53 -j ACCEPT iptables -A INPUT -p tcp --dport 53 -j ACCEPT # TCP for zone transfers / large responses # Allow outbound DNS queries from this host iptables -A OUTPUT -p udp --dport 53 -j ACCEPT iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
MySQL / MariaDB
# Allow MySQL from a specific app server only iptables -A INPUT -p tcp --dport 3306 -s 10.0.0.20 -m conntrack --ctstate NEW -j ACCEPT # Block MySQL from everywhere else iptables -A INPUT -p tcp --dport 3306 -j DROP
PostgreSQL
# Allow PostgreSQL from app subnet only iptables -A INPUT -p tcp --dport 5432 -s 10.0.0.0/24 -m conntrack --ctstate NEW -j ACCEPT iptables -A INPUT -p tcp --dport 5432 -j DROP
Redis
# Redis — bind to localhost only is safer; if exposed: iptables -A INPUT -p tcp --dport 6379 -s 127.0.0.1 -j ACCEPT iptables -A INPUT -p tcp --dport 6379 -j DROP
NTP
# Allow outbound NTP queries iptables -A OUTPUT -p udp --dport 123 -j ACCEPT iptables -A INPUT -p udp --sport 123 -m conntrack --ctstate ESTABLISHED -j ACCEPT # NTP server — accept client queries iptables -A INPUT -p udp --dport 123 -j ACCEPT
06 — NAT & Routing

NAT rules live in the nat table. Common uses: masquerading for internet sharing, port forwarding (DNAT), and load balancing. You must first enable IP forwarding in the kernel.

Enable IP forwarding
# Temporary (lost on reboot) echo 1 > /proc/sys/net/ipv4/ip_forward # Permanent — add to /etc/sysctl.conf or /etc/sysctl.d/99-forward.conf net.ipv4.ip_forward = 1 # Apply immediately sysctl -p
Masquerade / Internet sharing (SNAT with dynamic IP)
# All traffic leaving eth0 gets the public IP of eth0 (MASQUERADE) # eth1 is the LAN-facing interface iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # Allow forwarding between interfaces iptables -A FORWARD -i eth1 -o eth0 -m conntrack --ctstate NEW -j ACCEPT iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
SNAT — fixed source IP
# Rewrite source to a fixed public IP (better than MASQUERADE on static IPs) iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 203.0.113.5
DNAT — port forwarding
# Forward inbound TCP:80 to an internal web server iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 \ -j DNAT --to-destination 192.168.1.10:80 # Allow the forwarded traffic through the FORWARD chain iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 \ -m conntrack --ctstate NEW -j ACCEPT # Forward a non-standard external port to a different internal port # External :8443 → internal 192.168.1.20:443 iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8443 \ -j DNAT --to-destination 192.168.1.20:443
Redirect (transparent proxy / local port redirect)
# Redirect all outbound HTTP to a local Squid proxy on port 3128 iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 3128 # PREROUTING redirect for traffic from other hosts iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 3128
07 — Rate Limiting & DDoS Mitigation

iptables can slow brute-force attacks and connection floods using the limit, recent, and hashlimit modules — without external tools.

Limit new TCP connections per source IP (hashlimit)
# Allow each source IP no more than 10 new SSH connections per minute iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m hashlimit \ --hashlimit-name ssh \ --hashlimit-above 10/minute \ --hashlimit-mode srcip \ --hashlimit-burst 5 \ -j DROP iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
SYN flood protection
# Drop excessive SYN packets iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT iptables -A INPUT -p tcp --syn -j DROP # Enable kernel-level SYN cookies (persistent) echo "net.ipv4.tcp_syncookies = 1" >> /etc/sysctl.d/99-hardening.conf sysctl -p /etc/sysctl.d/99-hardening.conf
Port scan / stealth scan detection
# XMAS scan iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP # NULL scan iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP # FIN scan iptables -A INPUT -p tcp --tcp-flags ALL FIN -j DROP # Invalid state packets iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
ICMP flood protection
# Allow ping but cap rate iptables -A INPUT -p icmp --icmp-type echo-request \ -m limit --limit 1/s --limit-burst 5 -j ACCEPT iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
Block specific countries with ipset (CIDR list)
# Install ipset sudo apt install ipset # Create a set and populate it sudo ipset create blocklist hash:net sudo ipset add blocklist 203.0.113.0/24 sudo ipset add blocklist 198.51.100.0/24 # Use the set in iptables iptables -A INPUT -m set --match-set blocklist src -j DROP # Save ipset rules sudo ipset save > /etc/ipset.rules # Restore on boot (add to /etc/rc.local or a systemd unit) sudo ipset restore < /etc/ipset.rules
08 — Logging

The LOG target writes matched packets to the kernel log without affecting packet flow. Always put LOG rules before the DROP/ACCEPT rule for the same traffic.

Basic logging
# Log dropped INPUT packets (rate-limited to avoid log flooding) iptables -A INPUT -m limit --limit 5/min --limit-burst 10 \ -j LOG --log-prefix "iptables-DROP: " --log-level 4 # Log new SSH connections iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -j LOG --log-prefix "SSH-NEW: " --log-level 6 # Then allow iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
View log output
# Via dmesg dmesg | grep iptables # Via syslog grep "iptables" /var/log/syslog grep "iptables" /var/log/kern.log # Via journald journalctl -k | grep iptables
Log to a dedicated file (rsyslog)
# /etc/rsyslog.d/10-iptables.conf :msg, contains, "iptables-" /var/log/iptables.log & stop
09 — User-Defined Chains

User-defined chains let you group related rules, reduce repetition, and build modular rulesets. They act like subroutines — you jump to them from a built-in chain and they return when no rule matches.

Example — shared SSH rate-limit chain
# Create the chain iptables -N RATE-LIMIT-SSH # Populate it iptables -A RATE-LIMIT-SSH -m recent --set --name SSHTRACK iptables -A RATE-LIMIT-SSH -m recent --update --seconds 60 \ --hitcount 4 --name SSHTRACK \ -j LOG --log-prefix "SSH-RATELIMIT: " iptables -A RATE-LIMIT-SSH -m recent --update --seconds 60 \ --hitcount 4 --name SSHTRACK -j DROP iptables -A RATE-LIMIT-SSH -j ACCEPT # passes if not rate-limited # Jump to it from INPUT iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -j RATE-LIMIT-SSH
Example — shared blocklist chain
# Create iptables -N BLOCKLIST # Add known bad IPs iptables -A BLOCKLIST -s 203.0.113.0/24 -j DROP iptables -A BLOCKLIST -s 198.51.100.50 -j DROP iptables -A BLOCKLIST -j RETURN # return to caller if no match # Apply early in INPUT iptables -I INPUT 1 -j BLOCKLIST
10 — Persisting Rules

iptables rules are in-memory and are lost on reboot. There are several approaches to persist them across restarts.

iptables-persistent (Debian / Ubuntu)
# Install sudo apt install iptables-persistent # Save current rules (IPv4 and IPv6) sudo netfilter-persistent save # Saves to: /etc/iptables/rules.v4 and /etc/iptables/rules.v6 # Reload from saved files sudo netfilter-persistent reload
iptables-services (RHEL / CentOS)
sudo dnf install iptables-services sudo systemctl enable --now iptables # Save current rules sudo service iptables save # Saves to: /etc/sysconfig/iptables
Systemd unit (distro-agnostic)
# Save rules to a file sudo iptables-save > /etc/iptables/rules.v4 sudo ip6tables-save > /etc/iptables/rules.v6 # /etc/systemd/system/iptables-restore.service [Unit] Description=Restore iptables rules Before=network-pre.target Wants=network-pre.target [Service] Type=oneshot ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4 RemainAfterExit=yes [Install] WantedBy=multi-user.target
sudo systemctl enable --now iptables-restore
11 — IPv6 (ip6tables)

ip6tables has an identical syntax to iptables. Maintain separate but parallel rulesets for IPv4 and IPv6 — gaps in the IPv6 rules are a common source of firewall bypasses.

Minimal IPv6 stateful firewall
#!/bin/bash ip6tables -F; ip6tables -X # Default policies ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT ACCEPT # Loopback ip6tables -A INPUT -i lo -j ACCEPT # Established / Related ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # ICMPv6 — required for IPv6 to function correctly (NDP, path MTU, etc.) ip6tables -A INPUT -p ipv6-icmp -j ACCEPT ip6tables -A OUTPUT -p ipv6-icmp -j ACCEPT # SSH ip6tables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT # HTTP / HTTPS ip6tables -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j ACCEPT
⚠  Never block ICMPv6 entirely. Neighbour Discovery Protocol (NDP) — the IPv6 equivalent of ARP — depends on ICMPv6 types 133–137. Blocking all ICMPv6 breaks IPv6 connectivity completely.
12 — Troubleshooting
SymptomCause & Fix
Rules lost on rebootNot persisted — run netfilter-persistent save or service iptables save.
Locked out of SSHUse out-of-band console. Add your IP to an ACCEPT rule before setting DROP policy.
Rule added but traffic still blockedCheck rule order with iptables -L -n --line-numbers. An earlier DROP rule may match first.
NAT not workingIP forwarding disabled — sysctl net.ipv4.ip_forward must be 1.
FTP passive mode brokennf_conntrack_ftp module not loaded — modprobe nf_conntrack_ftp.
IPv6 traffic not filteredip6tables rules are separate — iptables rules do not apply to IPv6.
Counters not incrementingTraffic may be bypassing the host (switched locally) or hitting a different interface name.
REJECT vs DROP confusionDROP is silent (better for public); REJECT sends ICMP error (faster feedback for LAN).
Diagnostic commands
# List all rules with packet/byte counters iptables -L -v -n --line-numbers # Trace a packet through the ruleset (requires iptables-extensions) modprobe nf_log_ipv4 iptables -t raw -A PREROUTING -p tcp --dport 22 -j TRACE iptables -t raw -A OUTPUT -p tcp --dport 22 -j TRACE # Then watch: journalctl -k | grep TRACE # Check connection tracking table cat /proc/net/nf_conntrack conntrack -L # (requires conntrack-tools) # Count active rules per chain iptables -L INPUT --line-numbers | wc -l
13 — Quick Reference
CommandWhat it does
iptables -L -v -n --line-numbersList all filter rules with counters and line numbers
iptables -t nat -L -v -nList NAT table rules
iptables -FFlush all filter rules (dangerous — resets to policy)
iptables -P INPUT DROPSet default INPUT policy to DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPTAllow inbound SSH
iptables -I INPUT 1 -s <IP> -j DROPBlock a specific IP immediately (top of chain)
iptables -D INPUT <n>Delete rule at line number n
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADEEnable NAT masquerade on eth0
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to 192.168.1.10Forward port 80 to internal host
netfilter-persistent savePersist rules across reboots (Debian/Ubuntu)
iptables-save > rules.v4Export current ruleset to file
iptables-restore < rules.v4Import ruleset from file
ℹ  Consider migrating to nftables for new deployments — it unifies IPv4/IPv6 into a single ruleset, has a cleaner syntax, and is the long-term replacement for iptables on Linux.