iptables is a stateful firewall that controls network traffic into and out of a Linux system.

Let’s see what a basic web server setup looks like.

# Allow SSH access from anywhere
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow HTTP and HTTPS traffic from anywhere
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow established and related connections (crucial for return traffic)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop all other incoming traffic by default
sudo iptables -P INPUT DROP

This setup allows incoming SSH on port 22, HTTP on port 80, and HTTPS on port 443. The -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT rule is critical; it ensures that any incoming packets that are part of an already established connection (like responses to your outgoing web requests) are allowed. Without it, your web server wouldn’t be able to send responses back to clients. The DROP policy on the INPUT chain means that anything not explicitly allowed is blocked.

Now, let’s consider a common scenario: your web server needs to act as a gateway for other machines on a private network to access the internet. This is Network Address Translation (NAT).

# Enable IP forwarding
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# Allow SSH access from anywhere
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow HTTP and HTTPS traffic from anywhere
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow established and related connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Masquerade outgoing traffic from the private network (eth1)
# to the public network (eth0)
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# Drop all other incoming traffic by default
sudo iptables -P INPUT DROP

The echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward command enables packet forwarding, allowing the Linux box to route traffic between interfaces. The -t nat -A POSTROUTING -o eth0 -j MASQUERADE rule is the core of NAT. It tells iptables to modify the source IP address of outgoing packets on the eth0 interface (your public-facing interface) to be the IP address of eth0. This makes it appear to the internet that all traffic originating from your private network is coming from the web server itself. eth1 would be your private network interface.

A common mistake is forgetting to allow established and related connections. If you only allow new incoming connections for ports 80 and 443, your web server won’t be able to send responses back to the clients requesting web pages. The system would receive the request, but the response packet would be dropped because it’s not part of an explicitly allowed incoming connection.

Here’s a more granular approach for SSH, limiting access to a specific IP address.

# Allow SSH access ONLY from 192.168.1.100
sudo iptables -A INPUT -p tcp -s 192.168.1.100 --dport 22 -j ACCEPT

# Allow HTTP and HTTPS traffic from anywhere
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow established and related connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop all other incoming traffic by default
sudo iptables -P INPUT DROP

The -s 192.168.1.100 option specifies the source IP address that is permitted to connect to the SSH port. This is a much more secure practice than allowing SSH from 0.0.0.0/0 (anywhere).

When you are managing a firewall with many rules, the order matters significantly. iptables processes rules sequentially within a chain. The first rule that matches a packet determines the action taken. If you place your DROP rules too early, you might accidentally block legitimate traffic that comes later in your rule set. For instance, if you had:

sudo iptables -P INPUT DROP
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

This would immediately drop all incoming traffic, and the rule to accept SSH would never be reached. The correct order is typically to allow specific traffic first, then handle established/related connections, and finally set a default policy to drop everything else.

The MASQUERADE target is particularly useful for dynamic IP addresses. If your web server’s public IP address changes (e.g., in a DHCP environment), MASQUERADE automatically figures out the correct outgoing IP to use for translation. If you had a static public IP, you would use the SNAT target instead, specifying the IP: -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j SNAT --to-source 203.0.113.10.

To make these rules persistent across reboots, you’ll need to use a tool like iptables-persistent on Debian/Ubuntu systems:

sudo apt-get update
sudo apt-get install iptables-persistent
sudo netfilter-persistent save

Or iptables-services on RHEL/CentOS:

sudo yum install iptables-services
sudo systemctl enable iptables
sudo service iptables save

Understanding how to list and delete rules is also vital. sudo iptables -L -v -n will show you your rules with line numbers, packet/byte counts, and interfaces. To delete a rule, you use sudo iptables -D CHAIN_NAME RULE_SPECIFICATION, e.g., sudo iptables -D INPUT -p tcp --dport 80 -j ACCEPT.

The next common hurdle is dealing with ICMP (Internet Control Message Protocol) traffic, which is used for diagnostics like ping. By default, many firewalls block all ICMP, which can make network troubleshooting difficult. You’ll often want to allow specific ICMP types.

Want structured learning?

Take the full Iptables course →