✍️ 05-06: Writing iptables Rules¶
📌 General Rule Syntax¶
Every iptables rule follows roughly the same shape:
| 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:
- Allow all traffic on the loopback interface (local processes talking to themselves)
- Allow SSH management, but only from the trusted admin subnet
10.0.0.0/24 - Allow established/related traffic (so replies to connections we initiated always get through)
- 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.15on port22(SSH) is matched by rule 2 → ACCEPT. - A packet from
203.0.113.7on port22(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:
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:
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.
MASQUERADEis the dynamic-IP-friendly version of SNAT, commonly used for sharing one public IP among many internal hosts.- Extensions like the
limitmodule let iptables do basic rate limiting to blunt simple flooding attempts.