A comprehensive guide to iptables fundamentals, packet filtering architecture, and real-world security hardening strategies for production Linux servers. Learn how to build defense-in-depth firewall rules that protect your infrastructure.

Every production Linux server is under constant attack. Port scans, brute force attempts, DDoS floods, and exploit probes happen 24/7. Your application might be secure, but without proper network-level filtering, you're leaving the front door wide open.
iptables is the de facto firewall solution for Linux systems, sitting at the kernel level between your network interface and your applications. It's not just about blocking ports—it's about building intelligent, stateful packet filtering rules that form the first line of defense in your security architecture.
This isn't a beginner's tutorial. We're going deep into how iptables actually works, the packet flow through netfilter hooks, table and chain architecture, and most importantly, how to implement production-grade security hardening that you'd deploy on real infrastructure.
Before writing a single rule, you need to understand what happens when a packet hits your server.
iptables is the userspace tool that configures netfilter, the packet filtering framework built into the Linux kernel. When a packet arrives at your network interface, it doesn't go straight to your application—it flows through a series of decision points called hooks.
Think of netfilter hooks as security checkpoints. Each checkpoint examines the packet and decides whether to accept, drop, or modify it. There are five hooks in the packet journey:
PREROUTING - Packet just arrived, before routing decisionINPUT - Packet destined for local processFORWARD - Packet being routed through the systemOUTPUT - Packet generated by local processPOSTROUTING - Packet about to leave, after routing decisionThe routing decision happens between PREROUTING and INPUT/FORWARD. The kernel looks at the destination IP and decides: is this packet for me, or should I forward it to another host?
Here's the critical insight: different types of traffic flow through different hooks. A packet destined for your web server goes through PREROUTING → INPUT. A packet your server sends out goes through OUTPUT → POSTROUTING. A packet being routed through your server (if you're acting as a router/gateway) goes through PREROUTING → FORWARD → POSTROUTING.
iptables organizes rules into tables, and each table contains chains. This hierarchy exists because different operations need to happen at different stages of packet processing.
There are four main tables:
filter - The default table for packet filtering (accept/drop decisions). This is where most firewall rules live. Contains INPUT, FORWARD, and OUTPUT chains.
nat - Network Address Translation. Used for modifying source/destination addresses. Contains PREROUTING, OUTPUT, and POSTROUTING chains. This is where you'd configure port forwarding or masquerading.
mangle - Packet alteration for specialized routing. Can modify TTL, TOS, and other packet headers. Contains all five chains.
raw - Connection tracking exemptions. Processed before any other table. Contains PREROUTING and OUTPUT chains.
The processing order matters: raw → mangle → nat → filter. A packet hits raw first, then mangle, then nat, then filter.
Within each table, chains are the hook points. When you write a rule, you're adding it to a specific chain in a specific table. For example:
# This rule goes in the filter table, INPUT chain
iptables -A INPUT -p tcp --dport 22 -j ACCEPTRules are evaluated top-to-bottom. The first matching rule wins. If no rule matches, the chain's default policy applies (usually ACCEPT or DROP).
Every iptables rule has two parts: matching criteria and a target (action).
Matching criteria define what packets the rule applies to. You can match on:
-p tcp, -p udp, -p icmp)-s 192.168.1.0/24, -d 10.0.0.5)--sport 1024:65535, --dport 443)-i eth0, -o wlan0)-m state --state ESTABLISHED,RELATED)The target defines what happens when a packet matches. Common targets:
ACCEPT - Let the packet throughDROP - Silently discard the packetREJECT - Discard and send an error responseLOG - Log the packet and continue processingRETURN - Stop processing this chain, return to calling chainHere's where it gets interesting: connection state tracking. The -m state module (or newer -m conntrack) lets you write stateful firewall rules.
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTThis single rule allows all response traffic for connections you initiated. If your server makes an HTTP request to an external API, the response packets are automatically allowed back in. This is how modern firewalls work—you don't need to manually allow every possible response port.
Connection states:
NEW - First packet of a new connectionESTABLISHED - Part of an existing connectionRELATED - Related to an existing connection (like FTP data channel)INVALID - Packet doesn't match any known connectionThe default policy is what happens when a packet doesn't match any rule. This is your security baseline.
Most production systems use a "default deny" approach:
# Set default policies to DROP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPTThis means: drop all incoming traffic unless explicitly allowed, drop all forwarded traffic unless explicitly allowed, but allow all outgoing traffic.
Why allow OUTPUT by default? Because blocking outgoing traffic can break system functionality (DNS lookups, package updates, API calls). In high-security environments, you might set OUTPUT to DROP and whitelist specific outbound connections, but that requires careful planning.
Warning
Never set default policies to DROP without first adding rules to allow SSH access. You'll lock yourself out of the server. Always test firewall changes with a scheduled reboot or a failsafe script that flushes rules after 5 minutes.
Let's build a real-world firewall for a web server running nginx on port 80/443 and SSH on port 22.
First, flush existing rules and set default policies:
# Flush all rules
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPTAllow loopback traffic (critical for local services):
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPTAllow established and related connections:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTAllow SSH (with rate limiting to prevent brute force):
# Allow SSH but limit new connections to 3 per minute per 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
iptables -A INPUT -p tcp --dport 22 -j ACCEPTThis uses the recent module to track connection attempts. If an IP makes more than 3 new SSH connections in 60 seconds, subsequent attempts are dropped. This significantly slows down brute force attacks.
Allow HTTP and HTTPS:
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPTAllow ICMP ping (optional, but useful for monitoring):
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPTDrop invalid packets:
iptables -A INPUT -m conntrack --ctstate INVALID -j DROPLog dropped packets (for debugging):
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-dropped: " --log-level 4The --limit prevents log flooding. Without it, an attack could fill your disk with log entries.
SYN flood attacks exploit the TCP three-way handshake by sending SYN packets without completing the connection, exhausting server resources.
# Enable SYN cookies at kernel level
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
# Limit SYN packets per second
iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT
iptables -A INPUT -p tcp --syn -j DROPPort scanners send packets to multiple ports rapidly. Detect and block them:
# Detect port scanning
iptables -N port-scanning
iptables -A port-scanning -p tcp --tcp-flags SYN,ACK,FIN,RST RST -m limit --limit 1/s --limit-burst 2 -j RETURN
iptables -A port-scanning -j DROP
# Apply to INPUT chain
iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST RST -j port-scanningThis creates a custom chain that detects RST packets (common in port scans) and drops excessive attempts.
Block entire countries using ipset (more efficient than individual IP rules):
# Install ipset
apt-get install ipset
# Create a set for blocked countries
ipset create blocked-countries hash:net
# Add IP ranges (example: block a specific CIDR)
ipset add blocked-countries 203.0.113.0/24
# Block the set
iptables -A INPUT -m set --match-set blocked-countries src -j DROPIn production, you'd populate this with actual country IP ranges from databases like MaxMind.
For a database server that should only accept connections from application servers:
# Allow PostgreSQL only from app servers
iptables -A INPUT -p tcp -s 10.0.1.0/24 --dport 5432 -j ACCEPT
# Allow MySQL only from specific IPs
iptables -A INPUT -p tcp -s 10.0.1.10 --dport 3306 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.1.11 --dport 3306 -j ACCEPT
# Drop all other database traffic
iptables -A INPUT -p tcp --dport 5432 -j DROP
iptables -A INPUT -p tcp --dport 3306 -j DROPDocker manipulates iptables rules automatically, which can conflict with your custom rules. Docker adds rules to the FORWARD chain and creates its own chains.
iptables -L DOCKER -n -v
iptables -L DOCKER-USER -n -vTo add custom rules that apply to Docker containers, use the DOCKER-USER chain:
# Block external access to a container port
iptables -I DOCKER-USER -p tcp --dport 6379 ! -s 10.0.0.0/8 -j DROP
# Allow only specific IPs to access container
iptables -I DOCKER-USER -p tcp --dport 8080 -s 192.168.1.100 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 8080 -j DROPNetwork Address Translation is configured in the nat table. Common use cases:
Make internal network traffic appear to come from the gateway:
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Masquerade outgoing traffic
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADEForward external port to internal server:
# Forward external port 8080 to internal server port 80
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.100:80
# Allow forwarded traffic
iptables -A FORWARD -p tcp -d 192.168.1.100 --dport 80 -j ACCEPTDistribute traffic across multiple backends:
# Round-robin load balancing to three backends
iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 3 --packet 0 -j DNAT --to-destination 192.168.1.10:80
iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 2 --packet 0 -j DNAT --to-destination 192.168.1.11:80
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.12:80This is basic load balancing. For production, use dedicated load balancers like HAProxy or nginx.
iptables rules are not persistent by default. They're lost on reboot.
# Save current rules
iptables-save > /etc/iptables/rules.v4
# Install persistence package
apt-get install iptables-persistent
# Rules are automatically loaded from /etc/iptables/rules.v4# List all rules with line numbers
iptables -L -n -v --line-numbers
# Show rules in command format (easier to read)
iptables -S
# Show NAT rules
iptables -t nat -L -n -v# Delete by line number
iptables -D INPUT 5
# Delete by specification
iptables -D INPUT -p tcp --dport 8080 -j ACCEPTNever test firewall changes directly on a production server without a failsafe:
#!/bin/bash
# Apply new rules
iptables-restore < /etc/iptables/rules.v4.new
# Schedule rule flush in 5 minutes
at now + 5 minutes <<EOF
iptables -F
iptables -X
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
EOF
echo "Rules applied. You have 5 minutes to test."
echo "If everything works, cancel the scheduled flush with: atrm <job_id>"If you lose connectivity, the rules will automatically flush after 5 minutes.
New admins often write rules like this:
iptables -A INPUT -p tcp --dport 80 -j ACCEPTThis allows incoming connections to port 80, but without a rule for established connections, response packets might be dropped. Always include:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPTRules are evaluated top-to-bottom. This doesn't work:
iptables -A INPUT -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPTThe DROP rule matches everything first. SSH is never allowed. Correct order:
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROPAlways test SSH access before setting default DROP policy. Use a console or out-of-band access method.
Without logging, you're flying blind. You won't know what's being blocked or why connections fail.
iptables -A INPUT -p tcp -j ACCEPTThis allows all TCP traffic. Be specific about ports and sources.
iptables is fast, but inefficient rules can impact performance on high-traffic servers.
Instead of matching every packet individually, use connection tracking:
# Good: One rule handles all response traffic
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Bad: Separate rules for every possible response port
iptables -A INPUT -p tcp --sport 80 -j ACCEPT
iptables -A INPUT -p tcp --sport 443 -j ACCEPT
# ... hundreds more rulesMatching against thousands of IPs with individual rules is slow:
iptables -A INPUT -s 1.2.3.4 -j DROP
iptables -A INPUT -s 5.6.7.8 -j DROP
# ... thousands moreUse ipset instead:
ipset create blocklist hash:ip
ipset add blocklist 1.2.3.4
ipset add blocklist 5.6.7.8
iptables -A INPUT -m set --match-set blocklist src -j DROPipset uses hash tables for O(1) lookups instead of O(n) rule traversal.
Every packet is checked against every rule until a match is found. Put frequently-matched rules at the top:
# Most traffic is established connections - check this first
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Then common services
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Then less common services
iptables -A INPUT -p tcp --dport 22 -j ACCEPTiptables is powerful, but it's not always the right tool.
iptables operates at layers 3-4 (IP/TCP/UDP). It can't inspect HTTP headers, parse JSON, or make decisions based on application logic. For that, use application-level firewalls or reverse proxies like nginx, HAProxy, or Envoy.
If you're running on AWS, GCP, or Azure, use their native security groups instead of iptables. They're more integrated with the cloud platform, easier to manage at scale, and don't consume server resources.
nftables is the successor to iptables, offering better performance and cleaner syntax. It's been the default in newer Linux distributions. If you're starting fresh, learn nftables instead.
# iptables
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# nftables
nft add rule ip filter input tcp dport 22 acceptFor multi-gigabit networks or complex routing scenarios, dedicated hardware firewalls or specialized software like pfSense, OPNsense, or VyOS are more appropriate.
Here's a complete, production-ready firewall script for a typical web application server:
This script provides:
iptables -L -n -vThe packet and byte counters show how many times each rule has been matched. If a rule has zero hits, it might be misconfigured or unreachable.
iptables -ZUseful for testing specific rules.
tail -f /var/log/kern.log | grep iptables-dropped# Test TCP connection
nc -zv target-server 80
# Test with timeout
timeout 5 telnet target-server 443
# Check if port is filtered or closed
nmap -p 22,80,443 target-serverUse the TRACE target to see exactly which rules a packet matches:
# Load the module
modprobe nf_log_ipv4
# Enable tracing for specific traffic
iptables -t raw -A PREROUTING -p tcp --dport 80 -j TRACE
# Watch the trace
tail -f /var/log/kern.log | grep TRACEThis shows every table, chain, and rule the packet traverses. Remove the TRACE rule when done—it generates massive logs.
iptables is more than a firewall—it's a complete packet filtering and manipulation framework. Understanding the netfilter architecture, table/chain hierarchy, and connection state tracking is essential for building secure production systems.
The key principles:
Security is layered. iptables is your first line of defense, but it's not sufficient alone. Combine it with application-level security, intrusion detection, regular patching, and monitoring to build defense-in-depth.
Start with the production example script, adapt it to your specific services, and test thoroughly. Your infrastructure will be significantly more resilient against the constant barrage of attacks every internet-facing server endures.