iptables rules are stateful by default, meaning you don’t need to explicitly allow return traffic for established connections.

Let’s craft a robust iptables firewall script that you can adapt for various server setups. This isn’t just a collection of rules; it’s a template designed for clarity, security, and ease of modification.

#!/bin/bash

# --- Configuration ---
# Interface to protect (e.g., eth0, ens3)
INTERFACE="eth0"

# Allowed SSH port (default 22)
SSH_PORT="22"

# Allowed HTTP port (default 80)
HTTP_PORT="80"

# Allowed HTTPS port (default 443)
HTTPS_PORT="443"

# Trusted IP addresses/networks (e.g., your home IP, management subnet)
# Add multiple with spaces: TRUSTED_IPS="1.2.3.4 192.168.1.0/24"
TRUSTED_IPS=""

# Enable logging for dropped packets (0 for no, 1 for yes)
LOG_DROPPED="1"

# --- Script Logic ---

# Flush existing rules and chains
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X

# Set default policies to DROP (most secure)
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Usually safe to allow outgoing traffic

# --- Stateful Connection Tracking ---
# Allow all traffic for established and related connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# --- Loopback Interface ---
# Allow all traffic on the loopback interface
iptables -A INPUT -i lo -j ACCEPT

# --- SSH Access ---
# Allow SSH from trusted IPs
if [ -n "$TRUSTED_IPS" ]; then
    for ip in $TRUSTED_IPS; do
        iptables -A INPUT -p tcp -m tcp --dport "$SSH_PORT" -s "$ip" -j ACCEPT
    done
fi
# Allow SSH from any IP (use with extreme caution, consider limiting by IP if possible)
# iptables -A INPUT -p tcp -m tcp --dport "$SSH_PORT" -j ACCEPT

# --- HTTP/HTTPS Access ---
# Allow HTTP traffic
iptables -A INPUT -p tcp -m tcp --dport "$HTTP_PORT" -j ACCEPT

# Allow HTTPS traffic
iptables -A INPUT -p tcp -m tcp --dport "$HTTPS_PORT" -j ACCEPT

# --- ICMP (Ping) ---
# Allow essential ICMP types (echo-request, destination-unreachable, time-exceeded)
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT # echo-request (ping)
iptables -A INPUT -p icmp --icmp-type 3 -j ACCEPT # destination-unreachable
iptables -A INPUT -p icmp --icmp-type 11 -j ACCEPT # time-exceeded

# --- Logging Dropped Packets ---
if [ "$LOG_DROPPED" -eq 1 ]; then
    # Log dropped INPUT packets to a separate chain for clarity
    iptables -N LOGGING
    iptables -A INPUT -j LOGGING
    iptables -A LOGGING -m limit --limit 2/min --limit-burst 5 -j LOG --log-prefix "IPTables-Dropped: " --log-level 7
    iptables -A LOGGING -j DROP # Ensure dropped packets are actually dropped after logging
fi

# --- Apply rules to the specific interface ---
# This is often redundant if your default policy is DROP and you've explicitly allowed everything else,
# but can be useful for explicit interface binding.
# iptables -A INPUT -i "$INTERFACE" -j ACCEPT # This would override the DROP policy if not careful

# --- Save the rules (distribution dependent) ---
# For Debian/Ubuntu:
# sudo apt-get install iptables-persistent
# sudo netfilter-persistent save

# For RHEL/CentOS/Fedora:
# sudo service iptables save
# OR
# sudo iptables-save > /etc/sysconfig/iptables

echo "Firewall rules applied."
echo "Interface: $INTERFACE"
echo "SSH Port: $SSH_PORT"
echo "HTTP Port: $HTTP_PORT"
echo "HTTPS Port: $HTTPS_PORT"
echo "Trusted IPs: $TRUSTED_IPS"
echo "Logging Dropped: $([ "$LOG_DROPPED" -eq 1 ] && echo "Enabled" || echo "Disabled")"

Let’s break down why this works and how to tailor it.

Core Principles:

  1. Default Deny (DROP): The most critical security posture is to block everything by default. iptables -P INPUT DROP ensures that any traffic not explicitly allowed by a subsequent rule is discarded. This is far more secure than a REJECT policy, which sends back an error, potentially revealing information about your system.
  2. Stateful Inspection: iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT is the magic bullet for return traffic. Once a connection is established (e.g., you SSH into the server), the firewall remembers it. This rule allows all packets that are part of an existing, legitimate connection without you needing to create separate rules for outbound requests and their inbound replies.
  3. Least Privilege: Only open the ports and allow access from the IPs that are absolutely necessary. The template provides explicit rules for SSH, HTTP, and HTTPS, and a mechanism for trusted IPs.

Configuration Breakdown:

  • INTERFACE: This is the network interface your server uses to communicate with the outside world. You can find it using ip addr show or ifconfig. For example, on many cloud instances, it’s eth0.
  • SSH_PORT, HTTP_PORT, HTTPS_PORT: These variables make it trivial to change default ports if you’ve secured SSH by moving it to a non-standard port or if your web server uses different configurations.
  • TRUSTED_IPS: This is crucial for management. Add your home IP address or your office subnet here. This ensures that only these IPs can connect to sensitive services like SSH. If you need to allow multiple IPs or subnets, separate them with spaces.
  • LOG_DROPPED: Enabling this sends dropped packets to syslog (usually /var/log/syslog or /var/log/messages). This is invaluable for troubleshooting and security auditing. The limit module prevents your logs from being flooded.

Key Rule Explanations:

  • iptables -F, -X, -t nat -F, etc.: These commands flush (delete) all existing rules and user-defined chains. This ensures you’re starting with a clean slate, preventing conflicts with old rules.
  • iptables -P INPUT DROP: Sets the default policy for the INPUT chain to DROP. Packets arriving at the server that don’t match any subsequent rule will be silently discarded.
  • iptables -A INPUT -i lo -j ACCEPT: The lo interface is the loopback interface. Your system uses this to talk to itself. It’s essential for many local services, so we allow all traffic on it.
  • iptables -A INPUT -p tcp -m tcp --dport "$SSH_PORT" -s "$ip" -j ACCEPT: This is a specific rule.
    • -A INPUT: Appends this rule to the INPUT chain.
    • -p tcp: Matches the TCP protocol.
    • -m tcp --dport "$SSH_PORT": Matches the destination port (e.g., 22 for SSH).
    • -s "$ip": Matches the source IP address. This is where TRUSTED_IPS come in.
    • -j ACCEPT: If all conditions match, accept the packet.
  • iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT: Allows ping requests (ICMP type 8). Essential for network diagnostics. Including types 3 (destination-unreachable) and 11 (time-exceeded) helps with routing and network troubleshooting.
  • iptables -N LOGGING: Creates a new chain called LOGGING. This is a common technique to keep the main INPUT chain cleaner.
  • iptables -A INPUT -j LOGGING: Any packet that reaches this point in the INPUT chain (meaning it wasn’t accepted by any prior rule) is sent to the LOGGING chain.
  • iptables -A LOGGING -m limit --limit 2/min --limit-burst 5 -j LOG --log-prefix "IPTables-Dropped: " --log-level 7: In the LOGGING chain, this rule logs the packet. The limit module prevents log spam. log-prefix makes it easy to find these log entries.
  • iptables -A LOGGING -j DROP: After logging, the packet is dropped. This ensures that even if logging fails, the packet is still discarded.

The "One Thing" Nobody Tells You:

The iptables command itself doesn’t run the firewall persistently. It applies the rules in memory for the current session. To make them stick across reboots, you must save them. The method varies by distribution:

  • Debian/Ubuntu: Install iptables-persistent (sudo apt-get install iptables-persistent) and then run sudo netfilter-persistent save. This saves the current rules to /etc/iptables/rules.v4 and /etc/iptables/rules.v6.
  • RHEL/CentOS/Fedora (older): Use sudo service iptables save. This saves rules to /etc/sysconfig/iptables.
  • RHEL/CentOS/Fedora (newer, firewalld): If you’re using firewalld, iptables commands might be overridden. It’s generally better to stick to one or the other. If you must use iptables directly, you might need to disable firewalld and use iptables-services (sudo yum install iptables-services, sudo systemctl enable iptables, sudo systemctl start iptables, then sudo iptables-save > /etc/sysconfig/iptables).

Without saving, your firewall will vanish after the next reboot, leaving your server exposed.

Next Steps:

Once your basic firewall is solid, you’ll likely want to explore more advanced iptables features like rate limiting for specific services (e.g., to prevent brute-force attacks on SSH), blocking specific malicious IPs, or setting up port forwarding.

Want structured learning?

Take the full Iptables course →