iptables Firewall Rules Hands-on Configuration Guide: Linux Server Hardening
iptables is the Linux kernel native packet-filter firewall. This guide covers default policies, port allowlists, SSH source restrictions, port-scan defense, connection limits, persistence, and recovery.
iptables is the Linux kernel native packet-filter firewall. This guide covers default-deny policy, port allowlists, SSH source restrictions, port-scan defense, connection limits, persistence, and lockout recovery.
Applicable Scenarios
For admins who need iptables on a Linux server. Web, database, or API gateway—iptables is in-kernel, zero extra deps, fine-grained traffic control. Typical uses: restrict SSH sources, open specific ports, defend port scans, log anomalous traffic.
Prerequisites
- Linux server (CentOS 7+/Ubuntu 18.04+/Debian 10+)
- root or sudo
- SSH remote access (keep an out-of-band path before you start)
- iptables installed (preinstalled on most distros)
Check iptables:
iptables --version # should print something like iptables v1.8.7
How It Works
iptables is the userspace tool for kernel Netfilter. Core concepts:
- Tables: filter, nat, mangle, raw, security (SELinux)
- Chains: each table has built-in chains such as INPUT, OUTPUT, FORWARD
- Rules: matched in order; first match runs the target (ACCEPT/DROP/REJECT/LOG, etc.)
- Policy: chain default when no rule matches
Core strategy: default deny, allow as needed—set INPUT policy to DROP, then add allow rules. Minimizes attack surface.
Procedure
Step 1: List current rules
iptables -L -n -v --line-numbers
Flags: -L list, -n numeric (no DNS), -v counters (packets/bytes), --line-numbers for later insert/delete.
Step 2: Flush and set default policy
⚠️ Dangerous: these commands flush all rules. Remote SSH with a bad policy drops your session immediately.
# Flush all rules
iptables -F
iptables -X
iptables -Z
# Default policy: DROP INPUT and FORWARD, ACCEPT OUTPUT
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
Step 3: Allow loopback and established connections
# Allow loopback (many services depend on lo)
iptables -A INPUT -i lo -j ACCEPT
# Allow ESTABLISHED and RELATED
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
The second rule is critical—it lets replies for outbound connections (including your SSH session) through instead of hitting DROP.
Step 4: Open SSH
Under DROP you must explicitly allow SSH. Replace 22 with your actual SSH port:
# Restrict SSH to a specific subnet (safer)
iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
# No fixed IP: allow all sources (not recommended in production)
# iptables -A INPUT -p tcp --dport 22 -j ACCEPT
Always use `-s` to shrink the SSH attack surface. If you use a bastion, allow only the bastion IP.
Step 5: Open web ports
# HTTP (80)
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# HTTPS (443)
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Nginx proxying other services may need extra ports
# iptables -A INPUT -p tcp --dport 8080 -s 127.0.0.1 -j ACCEPT
Step 6: Rate-limit ICMP (ping)
# Rate-limit ICMP to mitigate ping flood
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
First rule allows 1 ping/sec; excess hits the second rule and is dropped. Keeps reachability checks, blocks ICMP flood.
Step 7: Defend against port scans
# Log and drop SYN scans (half-open)
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j LOG --log-prefix "NULL_SCAN: "
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
# Log and drop Xmas scans
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j LOG --log-prefix "XMAS_SCAN: "
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
# Cap concurrent connections per IP to prevent exhaustion
iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 100 --connlimit-mask 32 -j LOG --log-prefix "CONN_LIMIT: "
iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 100 --connlimit-mask 32 -j DROP
connlimit caps concurrent connections per IP (100 in the example). connlimit-mask 32 counts per IPv4 address (/32). For a /24, set 24.
Step 8: Log dropped traffic
# Log DROP hits to syslog (post-incident audit)
iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: " --log-level 4
# Place this after all ACCEPT rules and before the default policy
# It only runs when no ACCEPT rule matched
Logs go to `/var/log/messages` or `/var/log/syslog`. Inspect with `grep "IPTABLES-DROP" /var/log/syslog`.
Step 9: Persist rules
# CentOS/RHEL
yum install -y iptables-services
service iptables save
systemctl enable iptables
# Ubuntu/Debian
apt-get install -y iptables-persistent
netfilter-persistent save
Verification
# Full rule list
iptables -L -n -v --line-numbers
# Port reachability (local or remote host)
curl -I http://SERVER_IP # should return an HTTP response
ssh user@SERVER_IP # should log in
ping -c 3 SERVER_IP # should get replies (rate-limited 1/s)
# Closed ports should be denied
curl -I http://SERVER_IP:3306 # should time out or refuse
nc -zv SERVER_IP 3306 # should show connect failure
FAQ
Q1: SSH dropped after applying rules?
If remote SSH died because of the new rules:
- Out-of-band: iDRAC, IPMI, or physical console
- Safety crontab before you start: `*/5 * * * * root iptables -F` (flush every 5 minutes; gives you a window)
- Reboot: if rules were not saved, reboot restores empty defaults (still needs physical/OOB access)
Q2: DROP vs REJECT?
DROP silently discards; the client waits until timeout (safer; no port disclosure). REJECT sends an error (e.g. ICMP port-unreachable); the client fails immediately. Prefer DROP in production so scanners cannot infer port state from REJECT replies.
Q3: Does rule order matter?
Yes. iptables matches top to bottom and stops on first hit. Put ACCEPT rules first, DROP/REJECT later. An ACCEPT after a matching DROP never runs.
Summary
iptables is the baseline for Linux server hardening. Default deny plus source IP restrictions, connection limits, and drop logs shrinks the attack surface and surfaces scans. Pair with fail2ban for automated IP bans and defense in depth.