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 machine
  • OUTPUT — packets originating from this machine
  • FORWARD — 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 through
  • DROP — silently discard
  • REJECT — discard and send an error back
  • LOG — 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. ☕