The most surprising thing about iptables is that it’s not really about packets at all; it’s about connections.

Let’s see it in action. Imagine we want to allow established SSH connections into our server but block all new incoming SSH attempts from unknown sources.

First, we need to enable connection tracking. This is usually on by default, but it’s good to be sure.

sysctl net.netfilter.nf_conntrack_tcp_loose=1
sysctl net.netfilter.nf_conntrack_max=65536

The nf_conntrack_tcp_loose setting allows tracking of slightly malformed TCP packets, which is often necessary for real-world traffic. nf_conntrack_max sets the maximum number of connections the system can track simultaneously.

Now, let’s build our rules. The core of stateful firewalling is the conntrack module within iptables.

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

# Allow all outgoing traffic
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow new SSH connections (port 22) from anywhere, but only if they are NEW
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

# Drop everything else
iptables -P INPUT DROP
iptables -P FORWARD DROP

Here’s the breakdown:

  • iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT: This is the bedrock of stateful filtering. When a packet arrives, conntrack checks its state. If the packet is part of an existing, ongoing connection (ESTABLISHED) or is related to one (like an ICMP error for an established connection, or FTP data connections), it’s allowed in. This means you don’t need to explicitly open ports for the return traffic of your outgoing connections.
  • iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT: Similarly, for outgoing traffic, we allow packets that are part of existing connections or are related. This is often redundant if you have a permissive OUTPUT policy, but it’s good practice to be explicit.
  • iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT: This rule specifically targets new incoming TCP connections destined for port 22 (SSH). The --ctstate NEW is crucial. It means this rule only applies to the very first packet of a new TCP connection attempt. Once that connection is established, subsequent packets will match the ESTABLISHED,RELATED rule above and be allowed without hitting this NEW rule again.
  • iptables -P INPUT DROP and iptables -P FORWARD DROP: These are the default policies. Anything that doesn’t match an explicit ACCEPT rule (and isn’t ESTABLISHED or RELATED) will be dropped.

The magic lies in how conntrack tracks the lifecycle of connections. For TCP, it understands the three-way handshake (SYN, SYN-ACK, ACK). When a SYN packet arrives from an external source, conntrack marks it as NEW. If the server responds with SYN-ACK and the client acknowledges with ACK, conntrack transitions the connection state to ESTABLISHED. From that point on, all packets belonging to that connection are recognized as ESTABLISHED and are allowed by the first rule, regardless of their port or source IP (as long as they are part of that specific, tracked connection).

The RELATED state is for protocols that might spawn secondary connections. FTP is a classic example: the control connection (port 21) is established, but then a separate data connection is opened. conntrack can often infer the port and IP for the data connection based on the control connection and mark it as RELATED.

You can see the state table with conntrack -L. For example, to see active TCP connections:

conntrack -L -p tcp

This output will show you the source and destination IPs/ports, the protocol, and critically, the state (NEW, ESTABLISHED, RELATED, INVALID, etc.) that iptables is using.

You might have noticed that we didn’t explicitly allow outgoing NEW connections. This is because the default OUTPUT policy is usually ACCEPT. If you were to lock down your OUTPUT chain, you’d add a similar rule:

iptables -A OUTPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
iptables -P OUTPUT DROP # If you want to lock down outgoing too

This demonstrates how conntrack allows you to build a very granular firewall. You declare what’s allowed in (NEW connections on specific ports) and then let the ESTABLISHED,RELATED rule handle all the subsequent traffic for those connections, as well as the return traffic for your own outgoing connections.

The most common pitfall is forgetting to add the ESTABLISHED,RELATED rule. Without it, even if you allow NEW connections, the return packets will be dropped because they won’t match any explicit ACCEPT rule and will be subject to the default DROP policy.

The next hurdle is understanding how conntrack handles UDP and other connectionless protocols, which don’t have a formal handshake.

Want structured learning?

Take the full Iptables course →