Skip to content

✍️ 05-06: Writing iptables Rules


📌 General Rule Syntax

Every iptables rule follows roughly the same shape:

iptables [-t table] -A <chain> <match-criteria> -j <target>
Part Meaning
-t table Which table to add the rule to (defaults to filter if omitted)
-A <chain> Append the rule to the end of the named chain (e.g., INPUT, OUTPUT, FORWARD)
<match-criteria> The conditions a packet must meet (source/destination, port, protocol, state, etc.)
-j <target> Jump — what to do with a matching packet

💡 Rules in a chain are evaluated top to bottom, and the first match wins — once a packet matches a rule and that rule has a terminating target like ACCEPT/DROP, no further rules in that chain are checked. Order matters enormously.


🎯 Targets: ACCEPT, DROP, and REJECT

Target Behavior When to Use
ACCEPT Let the packet through For traffic you want to allow
DROP Silently discard the packet — sender gets no response at all Best for a hardened, "stealthy" default-deny policy — attackers scanning your host get no confirmation it even exists
REJECT Discard the packet, but send back an error (e.g., ICMP port unreachable or a TCP RST) Useful when you want the sender to know immediately that the connection was refused, rather than waiting for a timeout
RETURN Stop evaluating the current chain and return to the calling chain (used with custom user-defined chains) For structuring rule sets into reusable sub-chains
LOG Log the packet to the kernel log (dmesg/syslog), then continue to the next rule For auditing/debugging — LOG doesn't stop processing, so it's usually paired with a following DROP/ACCEPT rule matching the same criteria

Example: DROP vs. REJECT in Practice

# Silently drop anything from a known-bad IP — attacker's connection just hangs/times out
sudo iptables -A INPUT -s 198.51.100.66 -j DROP

# Reject connections to a closed internal service with an explicit "unreachable" message
sudo iptables -A INPUT -p tcp --dport 8080 -j REJECT --reject-with tcp-reset

💡 DROP is generally preferred for perimeter/Internet-facing rules (it makes port scanning/reconnaissance slower and less informative for an attacker). REJECT is often nicer for internal networks, where quick, clear failure feedback helps legitimate users and administrators troubleshoot faster.


🧩 Matching by Port and IP

Matching by Source/Destination IP

# Allow all traffic from a specific trusted host
sudo iptables -A INPUT -s 192.168.10.50 -j ACCEPT

# Allow all traffic from an entire trusted subnet (CIDR notation)
sudo iptables -A INPUT -s 192.168.10.0/24 -j ACCEPT

# Block a specific IP address outright
sudo iptables -A INPUT -s 203.0.113.99 -j DROP

Matching by Protocol and Port

# Allow inbound SSH (TCP port 22)
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

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

# Allow outbound DNS queries (UDP port 53)
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT

# Allow inbound ICMP echo-request (ping)
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

Combining Interface, IP, and Port

# Only allow SSH on the internal interface (eth1), never on the public one (eth0)
sudo iptables -A INPUT -i eth1 -p tcp --dport 22 -j ACCEPT

🏗️ Worked Example: A Small, Complete Rule Set

Let's build a realistic INPUT policy for a server that should:

  1. Allow all traffic on the loopback interface (local processes talking to themselves)
  2. Allow SSH management, but only from the trusted admin subnet 10.0.0.0/24
  3. Allow established/related traffic (so replies to connections we initiated always get through)
  4. Drop everything else
# 1. Always allow loopback traffic — many local services depend on it
sudo iptables -A INPUT -i lo -j ACCEPT

# 2. Allow SSH only from the trusted management subnet
sudo iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 22 -j ACCEPT

# 3. Allow return traffic for connections this host or a trusted client already started
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# (Optional but recommended) explicitly drop clearly invalid/malformed packets
sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# 4. Set the default policy to DROP everything else that reaches the end of the chain
sudo iptables -P INPUT DROP

Walking through what this achieves:

  • A packet from 10.0.0.15 on port 22 (SSH) is matched by rule 2 → ACCEPT.
  • A packet from 203.0.113.7 on port 22 (SSH) does not match rule 2 (wrong source subnet) → falls through, doesn't match ESTABLISHED/RELATED either (it's a NEW connection attempt) → falls through to the default policy → DROP.
  • A reply packet for a DNS query this host made outbound is recognized by conntrack as ESTABLISHED → matched by rule 3 → ACCEPT, with no separate rule needed for DNS replies.

⚠️ Order matters here: rule 3 (ESTABLISHED/RELATED) is placed before the default DROP but after the specific allow rules. If you put the default DROP policy in place before adding rule 2, you would lock yourself out of SSH management entirely — always add your allow rules first (see the safety note in 05-05: iptables Fundamentals).


🔀 NAT and Port Forwarding

The nat table lets you rewrite addresses/ports. The most common use case for beginners is DNAT (Destination NAT) — forwarding a connection arriving at your firewall/router to a different internal host and port.

Example: Port Forwarding (DNAT)

Suppose you have a public-facing Linux router with a single public IP, and you want anyone connecting to <public-ip>:8000 to actually be forwarded to an internal web server at 192.168.60.5:23:

sudo iptables -t nat -A PREROUTING -p tcp --dport 8000 \
    -j DNAT --to-destination 192.168.60.5:23

Because this rewrites the destination, it must happen in PREROUTING, before the kernel makes its routing decision (see the packet traversal diagram in 05-05: iptables Fundamentals). You'll also typically need a matching FORWARD rule to allow the now-redirected traffic through:

sudo iptables -A FORWARD -p tcp -d 192.168.60.5 --dport 23 -j ACCEPT

Example: Source NAT / Masquerading (Outbound)

If you want internal hosts on a private network to share the router's single public IP for outbound Internet access:

# Rewrite the source IP of outbound packets to the router's own public IP
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

MASQUERADE is a convenience form of SNAT that automatically uses whatever IP is currently assigned to the outbound interface — useful when that IP might change (e.g., DHCP), as opposed to SNAT --to-source <fixed-ip> which requires a static address.


🚧 Rate Limiting Example

iptables extensions (match modules) add extra capabilities. A common one is the limit module, useful for mitigating basic flooding:

# Allow at most 10 ICMP echo-requests per minute, with a small burst allowance
sudo iptables -A INPUT -p icmp -m limit --limit 10/min --limit-burst 5 -j ACCEPT

# Drop any ICMP that exceeds that rate
sudo iptables -A INPUT -p icmp -j DROP

💡 Notice the pattern: the first rule ACCEPTs traffic up to the limit, and only traffic exceeding it falls through to the second DROP rule. Order is what makes this work.


📌 Key Takeaways

  • The general rule form is iptables [-t table] -A <chain> <match> -j <target> — rules in a chain are checked top to bottom, first match wins.
  • ACCEPT allows, DROP silently discards (best for hardened perimeters), REJECT discards with an error message back to the sender (friendlier for internal troubleshooting).
  • Rules can match on source/destination IP (-s/-d), interface (-i/-o), protocol (-p), and port (--dport/--sport).
  • A solid minimal INPUT policy is: allow loopback, allow specific trusted NEW connections, allow ESTABLISHED/RELATED, then default DROP.
  • Always add allow rules before switching the default policy to DROP, especially when managing a remote host over SSH — order of operations can lock you out.
  • DNAT (port forwarding) rewrites destination addresses in PREROUTING; SNAT/MASQUERADE rewrites source addresses in POSTROUTING.
  • MASQUERADE is the dynamic-IP-friendly version of SNAT, commonly used for sharing one public IP among many internal hosts.
  • Extensions like the limit module let iptables do basic rate limiting to blunt simple flooding attempts.