Right, let’s talk about writing iptables rules and making them stick. This is the part where most people’s hair starts to fall out, not because the concepts are hard, but because the tooling is a bit of a historical artifact. iptables itself is brilliant, but the way we save and load rules feels like it was designed by someone who assumed you’d never reboot your server. We’ll fix that.

First, a crucial piece of context that everyone misses until it bites them: iptables rules are not configuration files. They are a runtime configuration for the Linux kernel’s netfilter system. When you run an iptables command, you’re changing the in-memory ruleset right now. This is fantastic for immediate testing and utterly useless for surviving a reboot. The kernel doesn’t care about your /etc directory. This is the fundamental disconnect we have to bridge.

The Vanilla Way: iptables-save and iptables-restore

So, how do we take our beautifully crafted runtime rules and make them permanent? We serialize them. The iptables-save command takes a snapshot of the current, in-memory rules and outputs them in a format that its sibling, iptables-restore, can read back in and execute line-by-line.

Think of iptables-save as taking a photograph of your rules. The photograph itself does nothing. iptables-restore is the project that brings that photograph to life, painstakingly recreating every rule, chain, and setting in the kernel’s memory.

Here’s the golden path:

  1. Carefully craft your rules using the iptables command. Test the living daylights out of them.
  2. Once it’s working, take your snapshot and save it to the canonical location. You need to do this as root, obviously.
sudo iptables-save | sudo tee /etc/iptables/rules.v4
sudo ip6tables-save | sudo tee /etc/iptables/rules.v6

Why tee? Because iptables-save outputs to stdout. Using tee both prints it to your screen (so you can see it) and writes it to the file. It’s a good way to avoid silent failures.

Now, how do we make the system load these on boot? This is where the “it depends” of Linux distros comes in. On Debian/Ubuntu, the iptables-persistent package is your friend. Install it, and it will automatically ask to save your current rules to /etc/iptables/rules.v4 and rules.v6. It also installs a service that runs iptables-restore on boot.

On other distros, you might need to install a service manually or hack a line into /etc/rc.local. It’s a bit janky, I know. The designers of this system clearly believed in manual labor.

The Common Pitfall: Atomic vs. Incremental

Here’s the critical insight that causes 90% of the problems: iptables-restore is atomic. It loads the entire ruleset in one go, and it’s designed to do so from a clean slate. This is its greatest strength and the source of most user error.

When you run iptables -A INPUT -s 192.168.1.100 -j ACCEPT, you’re making an incremental change. You’re saying, “Hey kernel, please add this one rule to the end of the INPUT chain.” iptables-restore does not do this. It says, “Hey kernel, delete every single rule you currently have and replace them all with this exact list.”

This is why you must always, always save the entire desired ruleset. If you manually add a rule and then only save that one rule to the file, iptables-restore will happily wipe your entire firewall configuration and replace it with that single, lonely rule on boot. Congratulations, you now have an extremely specific and very insecure firewall.

The correct workflow is never to edit /etc/iptables/rules.v4 by hand. The correct workflow is to use the iptables command to test your changes incrementally, and when you’re satisfied, blast the entire current state to the file with iptables-save.

A Practical Example: The Lockdown and Save

Let’s say you want to set up a basic firewall: allow all outgoing, allow established connections, allow SSH, and drop everything else.

You’d test it live first:

# Flush old rules (start fresh)
sudo iptables -F

# Set default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

# Allow established/related connections (crucial!)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow loopback interface. Forgetting this breaks a lot of local stuff.
sudo iptables -A INPUT -i lo -j ACCEPT

# Allow SSH on port 22 (please tell me you changed this from the default)
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Now, the final INPUT rule is an implicit DROP because of the policy.
# Your firewall is now live and blocking everything but SSH and established traffic.

Test it. Can you still SSH in? Good. Can you ping the server? No, because we didn’t allow ICMP, and that’s fine—it’s a matter of policy. Once you’re certain it works, you save the entire working state, not just the commands you typed.

sudo iptables-save | sudo tee /etc/iptables/rules.v4

Now, on the next reboot, the iptables-persistent service will run iptables-restore < /etc/iptables/rules.v4 and your perfect rules will be recreated atomically. It’s not elegant, but it’s robust. It’s the digital equivalent of carefully handwriting a recipe instead of just tossing ingredients into a pot. One method gives you predictable, repeatable results. The other gives you soup. Probably bad soup.