| Title: Extending fail2ban on NixOS | |
| Author: Solène | |
| Date: 02 October 2022 | |
| Tags: linux nixos fail2ban security | |
| Description: This article shows how to create new filters in fail2ban | |
| on NixOS. | |
| # Introduction | |
| Fail2ban is a wonderful piece of software, it can analyze logs from | |
| daemons and ban them in the firewall. It's triggered by certain | |
| conditions like a single IP found in too many lines matching a pattern | |
| (such as a login failure) under a certain time. | |
| What's even cooler is that writing new filters is super easy! In this | |
| text, I'll share how to write new filters for NixOS. | |
| fail2ban GitHub project page | |
| NixOS official website | |
| # Terminology | |
| Before continuing, if you are not familiar with fail2ban, here are the | |
| few important keywords to understand: | |
| * action: what to do with an IP (usually banning an IP) | |
| * filter: set of regular expressions and information used to find bad | |
| actors in logs | |
| * jail: what ties together filters and actions in a logical unit | |
| For instance, a sshd jail will have a filter applied on sshd logs, and | |
| it will use a banning action. The jail can have more information like | |
| how many times an IP must be found by a filter before using the action. | |
| # Configuration | |
| ## Enabling fail2ban | |
| The easiest part is to enable fail2ban. Take the opportunity to | |
| declare IPs you don't want to block, and also block IPs on all ports if | |
| it's something you want. | |
| ``` | |
| services.fail2ban = { | |
| enable = true; | |
| ignoreIP = [ | |
| "192.168.1.0/24" | |
| ]; | |
| }; | |
| # needed to ban on IPv4 and IPv6 for all ports | |
| services.fail2ban = { | |
| extraPackages = [pkgs.ipset]; | |
| banaction = "iptables-ipset-proto6-allports"; | |
| }; | |
| ``` | |
| ## Creating new filters | |
| A filter is composed of one/many regex, and also a systemd journal unit | |
| in case you are pulling information from it instead of a log file. | |
| We will use the module `environment.etc` to create files in | |
| `/etc/fail2ban/filter.d/` directory, so they can be used in the jails. | |
| These are examples of filters you may want to use. They target very | |
| large, this may not be ideal for your use case, but can serve as a good | |
| start. | |
| ``` | |
| environment.etc = { | |
| "fail2ban/filter.d/molly.conf".text = '' | |
| [Definition] | |
| failregex = <HOST>\s+(31|40|51|53).*$ | |
| ''; | |
| "fail2ban/filter.d/nginx-bruteforce.conf".text = '' | |
| [Definition] | |
| failregex = ^<HOST>.*GET.*(matrix/server|\.php|admin|wp\-).* HTTP/\d.\d\"… | |
| ''; | |
| "fail2ban/filter.d/postfix-bruteforce.conf".text = '' | |
| [Definition] | |
| failregex = warning: [\w\.\-]+\[<HOST>\]: SASL LOGIN authentication faile… | |
| journalmatch = _SYSTEMD_UNIT=postfix.service | |
| ''; | |
| }; | |
| ``` | |
| ## Defining the jails using our new filters | |
| Now we can declare fail2ban jails with each filter we created. If you | |
| use a log file, make sure to have `backend = auto`, otherwise the | |
| systemd journal is used and this won't work. | |
| The most important settings are: | |
| * filter: choose your filter using its filename minus the `.conf` part | |
| * maxretry: how many times an IP should be reported before taking an | |
| action | |
| * findtime: how long should we keep entries to match in maxretry | |
| ``` | |
| services.fail2ban.jails = { | |
| # max 6 failures in 600 seconds | |
| "nginx-spam" = '' | |
| enabled = true | |
| filter = nginx-bruteforce | |
| logpath = /var/log/nginx/access.log | |
| backend = auto | |
| maxretry = 6 | |
| findtime = 600 | |
| ''; | |
| # max 3 failures in 600 seconds | |
| "postfix-bruteforce" = '' | |
| enabled = true | |
| filter = postfix-bruteforce | |
| findtime = 600 | |
| maxretry = 3 | |
| ''; | |
| # max 10 failures in 600 seconds | |
| "molly" = '' | |
| enabled = true | |
| filter = molly | |
| findtime = 600 | |
| maxretry = 10 | |
| logpath = /var/log/molly-brown/access.log | |
| backend = auto | |
| ''; | |
| }; | |
| ``` | |
| # Creating filters | |
| It's actually easy to create filters, fail2ban provides a good | |
| framework like automatic date and host detection, which make creating | |
| regex very easy. | |
| You can use the command `fail2ban-regex` to experiment with regexes on | |
| some logs. | |
| Here is an example of a log file that would contain an IP and an error | |
| message: | |
| ``` | |
| fail2ban-regex /var/log/someservice.log "<HOST> ERROR" | |
| ``` | |
| Here is an example of a systemd unit log that would contain an IP, then | |
| a space and a 403 error: | |
| ``` | |
| fail2ban-regex -m _SYSTEMD_UNIT=someservice.service systemd-journal "<HOST> 403" | |
| ``` | |
| You can analyze what lines matched or not with the flags | |
| `--print-all-matched` and `--print-all-missed`. | |
| I recommend you to read fail2ban man pages and --help output if you | |
| want to create filters. | |
| # Conclusion | |
| Fail2ban is a fantastic tool to easily create filtering rules to ban | |
| the bad actors. It turned out most rules didn't work out of the box, | |
| or were too narrow for my use case, so extending fail2ban was quite | |
| straightforward. |