About me: My name is Solène Rapenne, pronouns she/her. I like learning and sharing knowledge. Hobbies: '(BSD OpenBSD Qubes OS Lisp cmdline gaming security QubesOS internet-stuff). I love percent and lambda characters. OpenBSD developer solene@. No AI is involved in this blog.

Contact me: solene at dataswamp dot org or @solene@bsd.network (mastodon).

I'm a freelance OpenBSD, FreeBSD, Linux and Qubes OS consultant, this includes DevOps, DevSecOps, technical writing or documentation work. If you enjoy this blog, you can sponsor my open source work financially so I can write this blog and contribute to Free Software as my daily job.

Extending fail2ban on NixOS

Written by Solène, on 02 October 2022.
Tags: #linux #nixos #fail2ban #security

Comments on Fediverse/Mastodon

1. 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

2. 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.

3. Configuration §

3.1. 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";
  };

3.2. 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\" 404.*$
    '';

    "fail2ban/filter.d/postfix-bruteforce.conf".text = ''
      [Definition]
      failregex = warning: [\w\.\-]+\[<HOST>\]: SASL LOGIN authentication failed.*$
      journalmatch = _SYSTEMD_UNIT=postfix.service
    '';
  };

3.3. 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
    '';
  };

4. 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.

5. 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.