iptables essentials: a practical guide to Linux firewalling
Every Linux server connected to the internet needs a firewall. iptables is the one that ships with virtually every distribution, and even if you eventually move to nftables or firewalld, understanding iptables helps you understand what those tools are actually doing underneath.
This guide covers the concepts and the commands. Not every option, but enough to be useful on a real server.
Prerequisites
- A Linux server (Ubuntu, Debian, CentOS, any of them work)
- Root or sudo access
- A terminal and some patience
WARNING
Locking yourself out of your own server with a bad firewall rule is a rite of passage. It also sucks. If you’re working on a remote machine, keep a backup session open before you start changing rules.
What iptables does
iptables filters network traffic. It looks at each packet coming into or leaving your machine and decides whether to let it through, drop it, or do something else with it. The decisions are based on rules you define.
The name comes from the “tables” it uses to organize rules, and the “chains” within those tables that process packets in order.
Tables and chains
There are several tables, but you’ll mostly work with two:
filter is the default table. It handles what gets through and what doesn’t. It has three chains:
- INPUT handles packets destined for your machine
- FORWARD handles packets routed through your machine
- OUTPUT handles packets your machine sends out
nat handles address translation. You need it for port forwarding and masquerading. It has PREROUTING, POSTROUTING, and OUTPUT chains.
For most server work, you’re writing rules in the filter table’s INPUT chain. That’s where you decide which services the outside world can reach.
Checking current rules
Before changing anything, see what’s already there:
sudo iptables -L -n -v
The -n flag shows IP addresses and ports as numbers instead of trying to resolve hostnames. The -v adds packet counters. On a fresh install, you’ll probably see empty chains with a default ACCEPT policy.
If you want to see the raw commands that would recreate your current ruleset:
sudo iptables-save
This output is useful for backing up your rules or understanding exactly what’s configured.
Rule syntax
A basic iptables rule follows this pattern:
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
Breaking that down:
-A INPUTappends the rule to the INPUT chain-p tcpmatches TCP protocol--dport 22matches destination port 22-j ACCEPTjumps to the ACCEPT target (allow the packet)
You can insert rules at a specific position with -I instead of -A:
sudo iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT
That inserts the rule at position 1 in the chain, so it gets evaluated first.
Common targets
- ACCEPT allows the packet
- DROP silently discards it
- REJECT discards it and sends an error back to the sender
DROP is usually what you want for a firewall. REJECT tells the other side you’re not listening, which leaks information about your server being up and running.
Setting up basic rules
Here’s a starting ruleset for a web server:
# Allow established connections (important, don't skip this)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Drop everything else
sudo iptables -P INPUT DROP
The first rule is important. It allows packets that belong to connections you already initiated. Without it, your server would accept the initial SSH connection but drop the responses, and you’d get nowhere.
The loopback rule lets your machine talk to itself, which a lot of services depend on.
The last line sets the default policy to DROP. Anything not explicitly allowed gets blocked. That’s the secure approach: deny by default, allow what you need.
Saving rules
iptables rules are lost on reboot unless you save them. On Debian/Ubuntu:
sudo iptables-save > /etc/iptables/rules.v4
On CentOS/RHEL:
sudo service iptables save
Or install iptables-persistent to handle this automatically:
sudo apt install iptables-persistent
During installation it will ask if you want to save current rules. Say yes.
Deleting rules
To delete a rule, you need to know its position in the chain:
sudo iptables -L INPUT --line-numbers
Then delete by number:
sudo iptables -D INPUT 3
That deletes the third rule in the INPUT chain.
To flush all rules and start over:
sudo iptables -F
Be careful with this. If your default policy is DROP and you flush all rules, you just blocked everything.
Blocking an IP address
To drop all traffic from a specific IP:
sudo iptables -A INPUT -s 192.168.1.100 -j DROP
To block a whole subnet:
sudo iptables -A INPUT -s 10.0.0.0/8 -j DROP
You can also block outbound traffic to an IP:
sudo iptables -A OUTPUT -d 192.168.1.100 -j DROP
Rate limiting
SSH brute-force attacks are constant on any public server. Rate limiting helps:
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 4 -j DROP
This allows 3 new SSH connections per 60 seconds from the same IP. The fourth attempt gets dropped. Adjust the numbers to taste.
Port forwarding
To forward traffic from port 8080 on your server to port 80 on another machine:
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.50:80
sudo iptables -A FORWARD -p tcp -d 192.168.1.50 --dport 80 -j ACCEPT
You also need to enable IP forwarding:
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
To make it permanent, edit /etc/sysctl.conf and uncomment the net.ipv4.ip_forward=1 line.
Logging
You can log dropped packets for debugging:
sudo iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: "
Logs go to /var/log/kern.log or /var/log/messages depending on your distribution. Be careful with this in production, logging every dropped packet can fill your disk fast if you’re under attack.
A better approach is to log only specific traffic:
sudo iptables -A INPUT -p tcp --dport 23 -j LOG --log-prefix "TELNET-ATTEMPT: "
sudo iptables -A INPUT -p tcp --dport 23 -j DROP
Listing and inspecting rules
Show rules with packet counts:
sudo iptables -L -n -v
Show rules with line numbers:
sudo iptables -L INPUT -n --line-numbers
Show the raw commands:
sudo iptables-save
Show rules for a specific chain:
sudo iptables -L OUTPUT -n -v
A complete example
Here’s a ruleset for a server running SSH, a web server, and a database that should only be accessible from the local network:
# Flush existing rules
sudo iptables -F
# Default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow established connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow MySQL from local network only
sudo iptables -A INPUT -p tcp --dport 3306 -s 192.168.1.0/24 -j ACCEPT
# Log and drop everything else
sudo iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROP: "
sudo iptables -A INPUT -j DROP
This gives you a server that accepts SSH, web traffic, and local database connections, blocks everything else, and logs what it blocks.
Common mistakes
Forgetting the established connections rule. You add it, things work, you think it’s optional. It’s not. Without it, return traffic for your outbound connections gets blocked and everything breaks.
Setting DROP policy before adding ACCEPT rules. Do it in the other order. Set your allow rules first, then set the default policy. Otherwise you lock yourself out the moment you set the policy.
Not saving rules. You spend an hour crafting the perfect ruleset, the server reboots for a kernel update, and everything is gone. Save your rules.
Blocking yourself. If you’re connected via SSH and change the SSH port rule, you’re done. Keep a backup session open or use a console connection.
When to use something else
iptables works, but it’s showing its age. nftables is its replacement in newer kernels and has a cleaner syntax. firewalld provides a higher-level interface that’s easier to manage on distributions that support it. UFW (Uncomplicated Firewall) is Ubuntu’s attempt to make iptables less painful.
For a single server with simple needs, any of these work fine. For complex setups with lots of rules, nftables scales better. For servers managed by configuration tools like Ansible or Puppet, the tool usually handles whichever firewall backend you have.
But understanding iptables matters even if you use something else. The concepts are the same. nftables still has chains and rules. firewalld still uses netfilter under the hood. When something breaks or you need to debug a networking issue, knowing how packets flow through iptables helps you figure out where the problem is.
Remarks
iptables looks intimidating until you use it a few times. The commands are verbose, but the logic is straightforward: match packets, decide what to do with them. Start with a simple ruleset, test it, save it, and build from there. The examples above cover the common cases. For everything else, man iptables has the full list of match extensions and targets.
Related Posts
Vim survival guide for Linux admins
The minimum vim knowledge you need to survive on a Linux server. Editing config files without wanting to throw your keyboard.
Linux memory management: free, vmstat, and /proc
Understanding how Linux uses memory. Reading free correctly, using vmstat, and what /proc/meminfo actually tells you.
tmux: terminal multiplexing made simple
Using tmux to manage terminal sessions. Panes, windows, and why your SSH sessions should survive disconnects.