iptables: The Commands You Actually Need
iptables: The Commands You Actually Need
You spin up a VPS. First thing you do is apt install nginx. Second thing you should do — before anything else — is set up a firewall.
Most people skip it, or copy-paste rules they don't understand, or install ufw and call it a day.
This post is for everyone who wants to actually know what's happening.
What iptables Is
iptables is a userspace tool for configuring the Linux kernel's packet filtering framework, netfilter. When a packet arrives at your machine, the kernel runs it through a series of hooks. iptables lets you attach rules to those hooks — accept this, drop that, forward this elsewhere.
It's been the standard Linux firewall tool since the late 1990s. That's why every tutorial, every Stack Overflow answer, every ops runbook uses it. It's everywhere.
One important thing to know upfront: on modern distros (Debian 10+, Ubuntu 20.04+, RHEL 8+), iptables is usually iptables-nft — a compatibility layer that translates iptables commands into nftables rules under the hood. The syntax you use stays the same. The behavior is the same. But the underlying kernel subsystem has changed.
More on that in the next post.
Tables, Chains, Rules
Three concepts. Get these right and everything else makes sense.
Tables define the purpose of the rules:
filter— the default. Accept, drop, or reject packets. This is what you use for a firewall.nat— network address translation. Port forwarding, masquerading.mangle— modify packet headers. Rarely needed.raw— bypass connection tracking. Advanced use.
Chains are points in the packet's journey where rules are evaluated:
INPUT— packets destined for this machineOUTPUT— packets originating from this machineFORWARD— packets passing through (like a router, or WireGuard traffic)PREROUTING— before routing decisions (nat table)POSTROUTING— after routing decisions (nat table)
Rules are the actual decisions: match these packets, do this action.
Actions (called targets) are:
ACCEPT— let it throughDROP— silently discardREJECT— discard and send an error backLOG— log it (then continue to next rule)
The chain has a policy — the default action if no rule matches. For INPUT on a server, you want DROP as the policy. Anything not explicitly allowed gets dropped.
Check What's Running
Before you change anything, see what's there:
# List all rules with line numbers
iptables -L -n -v --line-numbers
# List rules in a specific chain
iptables -L INPUT -n -v --line-numbers
# List NAT rules
iptables -t nat -L -n -v
-n skips DNS lookups (faster). -v shows packet/byte counters. --line-numbers lets you reference rules by position when deleting.
A Real Server Ruleset
Here's a solid baseline for a Linux server. Read every line — each one is explained.
#!/bin/bash
# Flush existing rules and reset policies
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
# Default policies: drop everything, then allow what we want
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow established and related connections
# Without this, your outbound connections can't get responses back
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback (localhost traffic)
iptables -A INPUT -i lo -j ACCEPT
# Allow ICMP (ping) — useful for debugging, limit to avoid flood
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s -j ACCEPT
# Allow SSH — change 22 to your actual port if you've moved it
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP and HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Log dropped packets (optional but useful for debugging)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables DROP: " --log-level 4
Run this as a script. iptables -F flushes all rules. iptables -P INPUT DROP sets the default policy to drop. Then we build back up — only what we explicitly need.
The conntrack rule is critical. Without it, you set OUTPUT ACCEPT but your SSH session dies because the response packets from your outbound connections get blocked on the way back in. ESTABLISHED,RELATED lets return traffic through.
WireGuard Rules
If you followed the WireGuard post and set up wg0, add these:
# Allow WireGuard UDP port
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Allow traffic on the WireGuard interface
iptables -A INPUT -i wg0 -j ACCEPT
# Allow forwarding between WireGuard and other interfaces
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -j ACCEPT
Common Operations
Add a rule:
# Append to end of chain
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
# Insert at position 1 (top of chain)
iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT
Delete a rule:
# By rule content
iptables -D INPUT -p tcp --dport 8080 -j ACCEPT
# By line number (get numbers from --line-numbers)
iptables -D INPUT 3
Block a specific IP:
iptables -A INPUT -s 1.2.3.4 -j DROP
Block a subnet:
iptables -A INPUT -s 192.168.1.0/24 -j DROP
Rate limit connections (brute force protection):
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 5 --name SSH -j DROP
This allows up to 4 new SSH connection attempts per 60 seconds per IP. The 5th gets dropped.
Port forwarding (NAT):
# Forward external port 8080 to internal 192.168.1.10:80
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80
iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPT
Masquerade (for routing traffic through this machine):
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
Needed if this machine is acting as a router — for WireGuard hub setups, for example.
Saving Rules
Rules are lost on reboot unless you save them.
On Debian/Ubuntu:
apt install iptables-persistent
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
iptables-persistent installs a systemd service that loads these files at boot.
To save current rules after making changes:
iptables-save > /etc/iptables/rules.v4
To restore manually:
iptables-restore < /etc/iptables/rules.v4
On RHEL/CentOS:
service iptables save
# or
iptables-save > /etc/sysconfig/iptables
Troubleshooting
Locked myself out of SSH
If you're on a VPS, use the provider's console (not SSH) to access the machine. Then:
# Nuclear option — flush everything, open SSH
iptables -F
iptables -P INPUT ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
Then rebuild your ruleset carefully before setting INPUT DROP again.
Rule isn't working
Check rule order. iptables evaluates rules top to bottom and stops at the first match. If you have a DROP rule before your ACCEPT rule for the same traffic, the DROP wins.
iptables -L INPUT -n -v --line-numbers
Look at the line numbers. If your ACCEPT is line 5 and a DROP for the same traffic is line 2, move the ACCEPT above it:
iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT
See which rule is matching packets
# Watch counters update in real time
watch iptables -L INPUT -n -v
The packets/bytes counters increment as traffic hits each rule. You can see exactly which rule is catching your traffic.
Log dropped packets
iptables -A INPUT -j LOG --log-prefix "DROPPED: " --log-level 4
Then watch the logs:
journalctl -k -f | grep DROPPED
# or
tail -f /var/log/syslog | grep DROPPED
The IPv6 Problem
Everything above is IPv4 only. iptables has a separate tool for IPv6: ip6tables. Same syntax, different command.
You need to duplicate your entire ruleset:
ip6tables -P INPUT DROP
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
# ... and so on
This is one of iptables' biggest pain points. Two separate rulesets to keep in sync. It's one of the main reasons nftables exists — it handles IPv4 and IPv6 in one unified ruleset.
Honest Verdict
iptables works. It's battle-tested, documented exhaustively, and runs on everything. If you're managing existing infrastructure, you'll be reading and writing iptables rules for years.
But the cracks show at scale. Managing rules as individual commands is fragile. The IPv4/IPv6 split is annoying. There's no built-in way to do an atomic ruleset replacement — you flush, you're briefly unprotected, you re-add. For a personal server, that's fine. For production, it's a real concern.
That's why nftables exists. Same kernel subsystem underneath, cleaner interface on top.
If you're starting fresh today, nftables is worth learning — and that's exactly what the next post covers.
Go Try It
# See what rules are on your system right now
iptables -L -n -v --line-numbers
If it's empty or just ACCEPT everywhere, your machine has no firewall.
Pick a port your server is listening on, write a rule, test it. That hands-on loop — write rule, test, observe — is how this becomes instinct.
Compiled by AI. Proofread by caffeine. ☕