iptables is the classic interface to the Linux kernel’s packet filter. Higher-level tools — UFW, firewalld, Docker — all generate iptables (or nftables) rules underneath. Knowing iptables helps you read what those tools do and write rules they can’t express.

Modern distributions run iptables on top of nftables (iptables-nft); the commands are the same. For new configurations written by hand, consider nftables directly.

Tables, chains, rules

  • Tables group rules by purpose: filter (allow/block, the default), nat (address translation), mangle (packet changes), raw.
  • Chains are lists of rules applied at a point in packet processing. In filter:
    • INPUT — packets to this server
    • OUTPUT — packets from this server
    • FORWARD — packets routed through it (VPN gateways, Docker)
  • Rules match packets (protocol, port, address, state) and jump to a target: ACCEPT, DROP, REJECT, LOG, or another chain.

Rules are checked top to bottom; the first match decides. If none match, the chain’s policy applies.

View the current rules

sudo iptables -L -n -v --line-numbers
sudo iptables -S                     # as commands — easiest to read and copy
sudo iptables -t nat -L -n -v

A safe default-deny ruleset

Order matters — allow SSH before setting the policy to DROP, or you’ll cut your own connection. Do this from the VPS console if possible, or with a safety net (see below).

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

# Allow replies to connections we initiated
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop invalid packets
sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# Allow SSH, HTTP, HTTPS
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# Allow ping
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

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

Repeat with ip6tables for IPv6 (using -p ipv6-icmp for ping, and allowing ICMPv6, which IPv6 needs to function).

Safety net while experimenting

sudo iptables-save > /root/rules.backup
echo "iptables-restore < /root/rules.backup" | sudo at now + 5 minutes

If you lock yourself out, the old rules come back in five minutes. Cancel the job (atq, atrm <id>) once you’ve confirmed access. (Requires the at package.)

Common rules

# Allow a port from one IP only
sudo iptables -A INPUT -p tcp -s 203.0.113.5 --dport 3306 -j ACCEPT

# Allow a subnet
sudo iptables -A INPUT -s 10.0.0.0/24 -j ACCEPT

# Block an IP (insert at the top)
sudo iptables -I INPUT 1 -s 192.0.2.66 -j DROP

# UDP port (WireGuard)
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT

# Rate-limit new SSH connections: max 4 per minute per IP
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 5 -j DROP

The rate-limit pair must come before the general SSH accept rule.

DROP vs REJECT

  • DROP silently discards — the client waits until timeout. Common for public-facing filtering.
  • REJECT replies “connection refused” — faster feedback, friendlier inside your own network.

Delete and insert rules

sudo iptables -L INPUT --line-numbers
sudo iptables -D INPUT 5                         # by number
sudo iptables -D INPUT -p tcp --dport 8080 -j ACCEPT   # by specification
sudo iptables -I INPUT 3 -p tcp --dport 8443 -j ACCEPT # insert at position 3
sudo iptables -F                                 # flush all rules in filter (policies stay!)

Flushing with a DROP policy still in place blocks everything — reset policies to ACCEPT before flushing over SSH.

Logging

sudo iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT-DROP: " --log-level 4

Place it just before the end of the chain to log what’s about to be dropped. Messages appear in the kernel log (journalctl -k | grep IPT-DROP).

NAT: share a connection and forward ports

Masquerade traffic from a VPN subnet out of the public interface (used for WireGuard and OpenVPN servers):

sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i wg0 -j ACCEPT
sudo iptables -A FORWARD -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Forward a public port to an internal host:

sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.20:80
sudo iptables -A FORWARD -p tcp -d 10.0.0.20 --dport 80 -j ACCEPT

Make rules permanent

iptables rules live in memory and disappear at reboot.

Debian/Ubuntu:

sudo apt install iptables-persistent
sudo netfilter-persistent save        # writes /etc/iptables/rules.v4 and rules.v6

RHEL family: sudo dnf install iptables-services, then sudo service iptables save — though firewalld is the native tool there.

Living with other tools

  • UFW / firewalld manage their own chains; mixing manual rules with them gets confusing. Use their mechanisms for custom rules (UFW’s before.rules, firewalld rich rules or direct rules).
  • Docker adds chains (DOCKER, DOCKER-USER) and NAT rules. Put your own filters for container traffic in DOCKER-USER. Flushing all rules breaks container networking until Docker restarts.

Quick reference

Task Command
List rules iptables -S / iptables -L -n -v
Allow port -A INPUT -p tcp --dport 443 -j ACCEPT
Allow from IP -A INPUT -s IP -j ACCEPT
Block IP -I INPUT 1 -s IP -j DROP
Default deny -P INPUT DROP (after allowing SSH!)
Delete rule -D INPUT N
Save netfilter-persistent save

Simpler alternatives: UFW on Ubuntu/Debian, firewalld on RHEL-family systems. See also custom firewall rules.

  • iptables
  • Firewall
  • Linux