As of Debian Buster the default package filtering mechanism is nftables replacing iptables1. When using iptables I have always used the excellent Shorewall to help manage the firewall. Unfortunately Shorewall does not, and probably never will, support nftables2. Nftables has a number of improvements over iptables including features which provide alternatives for some of the most usefull features in Shorewall. However I still miss one, for me, very important feature: safe-restart.

Safe-restart attemtps to make it safe to edit firewalls using a remote shell. If an error occurs when reloading or if the new firewall rules lock out the administrator the firewall is reverted to its previous state. This is by no means a garantuee, there is always the possibility that, despite the safe-restart, the administrator is locked out. So when editing firewalls remotely the admin should always have a backup way of logging into the system (physiscal access, remote KVM, IPMI, etc.).

In this blog post I will go through the steps of writing a Bash script called nft-safe-reload which will offer similar functionality to Shorewall’s safe-restart. So the requirements are:

  1. If loading error occurs during reload the firewall must revert to its previous state
  2. After the firewall change the user must confirm the firewall rules are valid otherwise the firewall must revert to its previous state
  3. If the user is unable to confirm the new firewall rules the firewall must revert to its previous state
  4. Script must clean up any temporary files created

The first requirement is actually very easy to fulfil. Unlike iptables, nftables will atomically apply a ruleset. Meaning that the whole rule file will either be applied completely or not at all.

So the first iteration of the script is as simple as:

#!/bin/bash

RULES=/etc/nftables.conf

apply() {
        nft -f $RULES
}

apply;

Next up is confirming the configuration. In order to do that we first save the current configuration into a temporary file.

save_file=$(mktemp)

save() {
        echo "flush ruleset" >> $save_file
        nft list ruleset >> $save_file
}

By default mktemp will create a new file under the system tmp directory with a random name and permissions restricted to the current user. This protects the confidentiality and integrity of the saved rules. The ‘flush ruleset’ statement makes sure all previous rules are removed before the set is loaded.

Restoring is identical to applying.

restore() {
        nft -f $save_file
        echo "New configuration has been rejected and the old one restored"
}

We need a way to ask the user whether to continue or revert.

read_yesno() {
        read yn 2> /dev/null

        case "$yn" in
                y|Y)
                        return 0
                        ;;
                *)
                        return 1
                        ;;
        esac
}

Then putting it all together:

save;

if apply; then
        echo -n "Do you want to accept the new firewall configuration? [y/n] "
        if read_yesno; then
                echo "New configuration has been accepted";
        else
                restore;
                exit 2;
        fi
fi

This takes care of requirement number 2. However if the admin locks themselves out the script will wait indefinetly (or until the shell is killed because of timeout) and never revert the change.

Fortunately the builtin read supports a timeout.

TIMEOUT=10

read_yesno_with_timeout() {
        read -t $TIMEOUT yn 2> /dev/null

        case "$yn" in
                y|Y)
                        return 0
                        ;;
                *)
                        return 1
                        ;;
        esac
}

This will wait for 10 seconds to receive the user input and otherwise assume the non-yes case.

Finally we need to clean up the temporary files. In order to do that, even if the script is interrupted, we can use a trap:

cleanup() {
        rm -f $save_file
}

trap "cleanup" EXIT;

The complete script should now look something like this:

#!/bin/bash

save_file=$(mktemp)

cleanup() {
        rm -f $save_file
}

trap "cleanup" EXIT;

RULES=/etc/nftables.conf
TIMEOUT=10

read_yesno_with_timeout() {
        read -t $TIMEOUT yn 2> /dev/null

        case "$yn" in
                y|Y)
                        return 0
                        ;;
                *)
                        return 1
                        ;;
        esac
}

save() {
        echo "flush ruleset" >> $save_file
        nft list ruleset >> $save_file
}

apply() {
        nft -f $RULES
}

restore() {
        nft -f $save_file
        echo "New configuration has been rejected and the old one restored"
}

save;

if apply; then
        echo -n "Do you want to accept the new firewall configuration? [y/n] "
        if read_yesno_with_timeout; then
                echo "New configuration has been accepted";
        else
                restore;
                exit 2;
        fi
fi

Put this in /usr/local/sbin/nft-safe-reload and set the exec permission and you are good to go.

Obviously you shouldn’t trust a random script someone put on the internet. Although it works for me, there is no garantuee it will work for you. So never just execute a script of the internet until you have assured yourself it does what it promises and always make sure you have a backup way into your system when changing the firewall remotely.

  1. See Debian Wiki 

  2. See Tom Eastep’s statement on shorewall-users, I completely understand and respects Tom’s decision, actually I would consider myself very fortunate if I, by the time I am 71, am still contributing to the community even a fraction of what Tom is doing now