iptables restore: Restore Rules from File after Reboot
iptables restore: reload your firewall rules from a file with iptables-restore and make them survive a reboot on Debian 13 and current Ubuntu releases, using iptables-persistent, a systemd unit or an ifupdown hook. Including the limits of each approach, and why IPv6 is the part most guides leave out.
Why iptables Rules Disappear After a Reboot
iptables is not a config file. It is state inside the kernel, held in the netfilter tables of the running system. Power the machine down and that state is gone. After a reboot the filter chains start out empty, on Debian and Ubuntu normally with an ACCEPT policy. The host takes everything until something writes the rules back, and nothing in the iptables toolchain does that by itself. It has to be triggered at boot.
Since Debian 10 “buster”, iptables is only the front end. Underneath it drives the kernel’s nf_tables subsystem, selected through update-alternatives (Debian 10 release notes); Ubuntu followed with 20.10 (Ubuntu security documentation). For saving and loading this changes nothing. iptables-save and iptables-restore talk to the same backend that accepted the rules in the first place. Which rules are worth writing, and why choosing between REJECT and DROP is more than a matter of taste, is covered in iptables REJECT vs DROP.
Restore iptables rules from a file
If a rules file already exists, applying it takes a single command. Assume iptables-save has written the rules to /etc/iptables/rules.v4:
sudo iptables-restore < /etc/iptables/rules.v4
That replaces the entire ruleset right away. Unless you pass --noflush, iptables-restore clears the affected tables first (iptables-restore(8)). To get the same call after every reboot, pick one of the three methods below.
Three Ways to Survive a Reboot
The three common approaches differ less in convenience than in what they rely on: method 1 on ifupdown, method 2 on a Debian package, method 3 on systemd. If you would rather not think about it, take method 2 on Debian or Ubuntu. It is the shortest one and the only one that covers IPv4 and IPv6 together without extra work.
1. Using iptables-save and iptables-restore
iptables-save writes the running ruleset to standard output as text, iptables-restore reads it back. Those two commands are all you need to build persistence yourself. One thing first: the directory /etc/iptables/ is created by the iptables-persistent package. Without that package, create it with sudo mkdir -p /etc/iptables or the redirect in the next step fails.
Step 1: Save the rules
This writes the running IPv4 ruleset to a file:
sudo iptables-save > /etc/iptables/rules.v4
Step 2: Create the restore script
ifupdown runs everything in /etc/network/if-pre-up.d/ before it brings an interface up. That is where the script goes:
sudo nano /etc/network/if-pre-up.d/iptables-restore
File contents:
#!/bin/sh
iptables-restore < /etc/iptables/rules.v4
exit 0
Step 3: Make the script executable
Without the execute bit, ifupdown skips the file without complaining:
sudo chmod +x /etc/network/if-pre-up.d/iptables-restore
This only works if ifupdown actually manages the network, meaning the configuration lives in /etc/network/interfaces. On a default Debian install it does. On Ubuntu it no longer does. Since 17.10 Netplan configures the network, and Netplan has no hook scripts at all; its own FAQ points to networkd-dispatcher instead (Netplan FAQ, MigratingToNetplan). The same goes for desktops running NetworkManager. So if the script sits in the right place and nothing happens at boot, this is almost always why.
2. Using the iptables-persistent Package
On Debian and Ubuntu a package takes the work off your hands. iptables-persistent is only the plugin half. The loading is done by netfilter-persistent, a plugin-based loader that applies the rules at boot and writes them back on command (netfilter-persistent(8)). The package creates /etc/iptables/ and ships one plugin for IPv4 and one for IPv6.
Installing the Package
Install it:
sudo apt install iptables-persistent
The installer asks through debconf whether the currently loaded rules should be saved. Answering yes writes them to /etc/iptables/rules.v4 and /etc/iptables/rules.v6.
Updating Rules
Later changes take one command. It overwrites both files with whatever is in the kernel at that moment:
sudo netfilter-persistent save
At boot, netfilter-persistent.service starts and loads the files. Two details are worth knowing. The plugin creates the rules file with mode 0640, so it is not world-readable. And netfilter-persistent stop does not drop the rules; it points you to flush for that. That is deliberate. Otherwise a package upgrade would leave the host without a firewall for its duration. If you want the other behaviour, set FLUSH_ON_STOP in /etc/default/netfilter-persistent.
3. Using systemd Services
Without the package, a unit of your own will do. The appeal is limited, because you end up rebuilding what netfilter-persistent.service already is. It makes sense in slim images where no extra package is wanted, or when the rules come from a file other than rules.v4.
Step 1: Create Service File
Create the unit file:
sudo nano /etc/systemd/system/iptables-restore.service
Contents:
[Unit]
Description=Restore iptables firewall rules
DefaultDependencies=no
Wants=network-pre.target
Before=network-pre.target shutdown.target
After=local-fs.target
Conflicts=shutdown.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
[Install]
WantedBy=multi-user.target
Wants=network-pre.target is the line most guides leave out. network-pre.target is a passive target: network management software orders itself after it but does not pull it in. Write only Before= and the ordering has no effect, because nothing activates the target; the unit then runs at some point, possibly after the first packet has already arrived (systemd.special(7)). DefaultDependencies=no additionally keeps systemd from quietly placing the unit after basic.target. Both netfilter-persistent.service and nftables.service from the Debian packages use exactly this combination.
Step 2: Enable Service
Enable the service:
sudo systemctl daemon-reload
sudo systemctl enable iptables-restore.service
To check that systemd orders the unit the way you intended, run systemctl show -p Wants -p Before -p After iptables-restore.service. Verify the syntax with systemd-analyze verify /etc/systemd/system/iptables-restore.service, ideally before the reboot rather than after it.
Test the Ruleset Before It Locks You Out
A faulty ruleset applied over SSH can cost you access to the server. iptables-restore has --test for exactly that: the ruleset is parsed and built, but never committed.
sudo iptables-restore --test < /etc/iptables/rules.v4
netfilter-persistent does this on every start. The shipped /etc/default/netfilter-persistent already has IPTABLES_TEST_RULESET=yes and IP6TABLES_TEST_RULESET=yes enabled. If the test load fails, the plugin prints “Error: IPv4 rules failed test load. New rules NOT loaded” and leaves the existing rules alone.
The second point most guides skip: iptables-save covers IPv4 only. A host with an IPv6 address stays unfiltered on the second stack, no matter how clean the v4 rules are. The counterparts are ip6tables-save and ip6tables-restore. The file is /etc/iptables/rules.v6.
sudo ip6tables-save > /etc/iptables/rules.v6
And one obvious step that is skipped surprisingly often: reboot once on purpose, then compare sudo iptables -L -n -v against what you had before. For what is listening in the first place, see How to show used ports in Linux.
Troubleshooting and Common Issues
When the wrong rules show up after a reboot, or none at all, it is usually one of these three causes.
Problem: Rules Not Loading
Start with the service: systemctl status netfilter-persistent or systemctl status iptables-restore.service, plus journalctl -u netfilter-persistent -b for the last boot. If the rules file is missing, the plugin prints “Warning: skipping IPv4 (no rules to load)” and does not fail. The service then counts as successfully started even though not a single rule was loaded. With a hand-written script in /etc/network/if-pre-up.d/, the most common cause is the missing execute bit, the second most common that ifupdown is not involved at all.
Problem: Conflicts with Other Services
Docker, libvirt, ufw and fail2ban all write chains of their own. Docker creates DOCKER, DOCKER-USER and DOCKER-FORWARD among others in the filter table, another DOCKER chain in nat, and expects your own rules to go into DOCKER-USER (Docker documentation). Restoring a full ruleset while such a service is running wipes its chains. Without --noflush, iptables-restore empties the table completely. Either save only once every service is up, or load with --noflush and keep the file to your own chains.
Problem: Inconsistent Rules
Two mechanisms loading the same file at boot are not a safety net. Whichever runs last decides the ruleset. Once iptables-persistent is installed, the script in /etc/network/if-pre-up.d/ has to go and your own unit has to be disabled. There is a quieter variant of the same problem: iptables-legacy and iptables-nft keep separate rulesets. Anything created through the legacy backend is invisible to iptables-save in nft mode. update-alternatives --display iptables tells you which one is active.
Or Go Straight to nftables
On Debian 13 and current Ubuntu releases, iptables drives nf_tables anyway. If you have no existing iptables rules to maintain, you can skip the detour. nft list ruleset prints the ruleset, and /etc/nftables.conf is the file that nftables.service reads at boot. Persistence there is built in.
The flip side: do not mix the two worlds on one host. An nftables.service loading its file and a netfilter-persistent applying its own afterwards produce a state that neither ruleset describes on its own. The shipped /etc/nftables.conf also starts with flush ruleset. That clears whatever iptables put there earlier.
Conclusion
For Debian and Ubuntu the answer is short: install iptables-persistent, save changes with netfilter-persistent save, done, IPv6 included. A systemd unit of your own pays off when no extra package may enter the image. The ifupdown script only where the network still comes from /etc/network/interfaces. What stays the same in all three cases is the deliberate reboot afterwards. A ruleset you merely assume is in place after boot is not one.