Title: Automatically ban ports scanner IPs on NixOS | |
Author: Solène | |
Date: 29 September 2022 | |
Tags: linux security nixos firewall | |
Description: This article presents how to automatically block IPs | |
connecting to your server on undesirable ports. | |
# Introduction | |
Since I switched my server from OpenBSD to NixOS, I was missing a | |
feature. The previous server was using iblock, a program I made to | |
block IPs connecting on a list of ports, I don't like people knocking | |
randomly on ports. | |
iblock is simple, if you connect to any port on which it's listening, | |
you get banned in the firewall. | |
iblock project page | |
I reimplemented it using iptables on NixOS. | |
# How it works | |
Iptables provides a feature adding an IP to a set if the address | |
connects n times before s seconds. Let's just set it to ONCE so the | |
address is banned on first connection. | |
For the record, a "set" is an extra iptables feature allowing to add | |
many IP addresses like an OpenBSD PF table. We need separate sets for | |
IPv4 and IPv6, they don't mix well. | |
# The implementation | |
You can create a new nix file with this content and add it to the | |
imports of your configuration file. | |
``` | |
{ | |
lib, | |
pkgs, | |
... | |
}: let | |
wan_interface = "eth0"; | |
ports-to-block = "21,23,53,111,135,137,138,139,445,1433,25565,5432,3389,3306,… | |
# block people 10 days | |
expire = 60 * 60 * 24 * 10; # in seconds, 0 to disable expiration , max is 21… | |
rules = table: [ | |
"INPUT -i ${wan_interface} -p tcp -m multiport --dports ${ports-to-block} -… | |
"INPUT -i ${wan_interface} -p tcp -m multiport --dports ${ports-to-block} -… | |
"INPUT -i ${wan_interface} -p tcp -m set --match-set ${table} src -j nixos-… | |
"INPUT -i ${wan_interface} -p udp -m set --match-set ${table} src -j nixos-… | |
]; | |
create-rules = | |
lib.concatStringsSep "\n" | |
( | |
builtins.map (rule: "iptables -C " + rule + " || iptables -A " + rule) (r… | |
++ builtins.map (rule: "ip6tables -C " + rule + " || ip6tables -A " + rul… | |
); | |
delete-rules = | |
lib.concatStringsSep "\n" | |
( | |
builtins.map (rule: "iptables -C " + rule + " && iptables -D " + rule) (r… | |
++ builtins.map (rule: "ip6tables -C " + rule + " && ip6tables -D " + rul… | |
); | |
in { | |
networking.firewall = { | |
enable = true; | |
extraPackages = [pkgs.ipset]; | |
extraCommands = '' | |
if test -f /var/lib/ipset.conf | |
then | |
ipset restore -! < /var/lib/ipset.conf | |
else | |
ipset -exist create blocked hash:ip ${ | |
if expire > 0 | |
then "timeout ${toString expire}" | |
else "" | |
} | |
ipset -exist create blocked6 hash:ip family inet6 ${ | |
if expire > 0 | |
then "timeout ${toString expire}" | |
else "" | |
} | |
fi | |
${create-rules} | |
''; | |
extraStopCommands = '' | |
ipset -exist create blocked hash:ip ${ | |
if expire > 0 | |
then "timeout ${toString expire}" | |
else "" | |
} | |
ipset -exist create blocked6 hash:ip family inet6 ${ | |
if expire > 0 | |
then "timeout ${toString expire}" | |
else "" | |
} | |
ipset save > /var/lib/ipset.conf | |
${delete-rules} | |
''; | |
}; | |
} | |
``` | |
To explain this implementation without going into details: | |
* rules are generated for IPv4 and IPv6 | |
* rules are generated with a check if they exist before adding or | |
removing them | |
* ipset are created if they don't exist, and loaded / saved on disk in | |
/var/lib/ipset.conf on start / stop | |
# Caveat | |
The configuration isn't stateless, it creates a file | |
/var/lib/ipset.conf , so if you want to make changes like expiration | |
time to the sets while they already exist, you will need to use ipset | |
yourself. | |
And most importantly, because of the way the firewall service is | |
implemented, if you don't use this file anymore, the firewall won't | |
reload. | |
I've lost a lot of time figuring why: when NixOS reloads the firewall | |
service, it uses the new reload script which doesn't include the | |
cleanup from stopCommand, and this fails because the NixOS service | |
didn't expect anything in the INPUT chain. | |
``` | |
sept. 29 23:24:22 interbus systemd[1]: Reloading Firewall... | |
sept. 29 23:24:22 interbus firewall-reload[94376]: iptables: Chain already exis… | |
sept. 29 23:24:22 interbus firewall-reload[94340]: Failed to reload firewall...… | |
sept. 29 23:24:22 interbus systemd[1]: firewall.service: Control process exited… | |
sept. 29 23:24:22 interbus systemd[1]: Reload failed for Firewall. | |
``` | |
In this case, you have to manually delete the rules in the INPUT chain | |
in for IPv4 and IPv6, or reboot your system that will start with a | |
fresh set, or flush all rules in iptables and restart the firewall | |
service. | |
# Conclusion | |
I'll be able to publish again a list of IPs scanning my server, and | |
this is also fun to see the list growing every minute. |