Automatiser la création de filtres d'URL HTTP pour Fail2ban avec Ansible

Automatiser la création de filtres d'URL HTTP pour Fail2ban avec Ansible

Introduction

Sur un serveur web, il peut arriver que l'on souhaite bannir certaines URL pour plusieurs motifs (faille logiciel 0-day, endpoint critique exposé etc...). Pour cela, Fail2ban est un bon moyen de bannir les adresses IP de toute tentative d'accès à une URL. Nous allons voir comment automatiser la création de ces règles avec Ansible.

Les filtres Fail2ban

Avant d'automatiser, il faut comprendre le bon fonctionnement des filtres dans Fail2ban.
Partons du principe que l'on a un serveur web Nginx avec Fail2ban installé sur le serveur et que l'on souhaite bannir pendant 1 an, au bout de 5 tentatives, les requêtes HTTP entrantes avec la méthode GET sur l'endpoint /robots.txt ainsi que les requêtes POST sur l'endpoint /api/v2

Pour ce faire, il est nécessaire de créer 2 fichiers. Un fichier que l'on va nommer http-custom.conf qui sera situé dans le répertoire /etc/fail2ban/filter.d . Ce fichier ressemblera à cela :

[Definition]
failregex = ^<HOST>.*GET.*/robots.txt.*$
            ^<HOST>.*POST.*/api/v2.*$
ignoreregex =

La directive failregex prend en entrée une ou plusieurs regex qui, lorsqu'elle(s) matchera dans le fichier de log Nginx, ira bannir l'adresse IP du client au bout du nombre de tentatives que nous choisissons.

Le deuxième fichier que nous allons créer sera nommé http.conf qui se trouvera dans le répertoire /etc/fail2ban/jail.d et aura ce contenu :

[http-custom]
enabled = true
filter = http-custom
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 31536000
banaction = iptables-allports
port = anyport

Le rôle Ansible

Automatisons tout cela avec un rôle Ansible plutôt court et principalement basé sur des Templates Jinja2. Une seule task sera créée, uniquement pour la copie du Template sur le(s) serveur(s) distant.

Voici à quoi devrait ressembler votre rôle Ansible avec le playbook fail2ban.yml

.
└── fail2ban.yml/
    ├── fail2ban/
    ├── ├── defaults
    ├── │   └── main.yml
    ├── ├── handlers
    ├── │   └── main.yml
    ├── ├── tasks
    ├── │   └── main.yml
    └── └── templates/
        ├── ├── etc
        ├── │   ├── fail2ban
        ├── │   │   ├── filter.d
        ├── │   │   │   ├── http-custom.conf.j2
        ├── │   │   └── jail.d
        └── │   │       ├── http.conf.j2

Les variables

Afin d'automatiser toutes les ressources qui seront crées, nous allons nous appuyer sur une liste de variables YAML qui nous permettra de créer autant de filtre que nous souhaitons, spécifier de quel serveur web il s'agit etc.... Le choix a été fait de cette manière car cela permet notamment de connaître l'état des filtres en place.

Créons notre fichier defaults/main.yml :

# Pour chaque pattern, il y a une regex avec * avant et après
fail2ban:
  http:
    - method: GET
      patterns:
        - /robots.txt
        - /endpoint-numero-2
    - method: POST
      patterns:
        - /api/v2
    - method: PUT
      patterns:
        - /api/v2/
  webserver: apache   # Apache ou Nginx
  max_retries: 10     # Nombre maximal de tentatives avant le ban
  ban_days: 365       # Nombre de jours de banissement de l'IP
Variables

Vous avez compris le principe. Vous pouvez ajouter autant de patterns d'URL que vous le souhaitez, autant de méthodes HTTP etc...

Les templates Jinja2

La pierre angulaire de notre rôle se situe dans le fichier http-custom.conf et dans la regex qui est contenue dedans.

Place à la création du Template Jinja2 se trouvant dans templates/etc/fail2ban/filter.d/http-custom.conf.j2

#jinja2: lstrip_blocks: True, trim_blocks: False
[Definition]
failregex = {% for http in fail2ban.http -%}
{% set outer_loop = loop -%}
{% for pattern in http.patterns %}{% if not outer_loop.first or (outer_loop.first and not loop.first) %}{% filter indent(12) %}
^<HOST>.*{{ http.method }}.*{{ pattern }}.*${% endfilter %}{% else %}^<HOST>.*{{ http.method }}.*{{ pattern }}.*${% endif -%}
{% endfor -%}
{% endfor %}
ignoreregex =

Ce Template est assez complexe car la syntaxe de Fail2ban est très restrictive. En effet, si l'on souhaite avoir plusieurs regex dans la directive failregex, il faut absolument que chacune des regex soit alignée à l'espace près ! Il faut également 2 boucles for afin de parcourir la liste YAML qui est imbriquée.

La première ligne #jinja2: lstrip_block: True, trim_block: False est essentielle car Ansible dispose du moteur Jinja2 de manière un peu différente. Cette ligne sera donc interprétée et ne figurera pas dans le Template final qui sera déployé.

Le deuxième Template Jinja2 est le fichier qui va venir créer un "jail" qui définit les règles de banissement pour un filtre donné. Il s'agit du fichier templates/etc/fail2ban/jail.d/http.conf.j2

[http-custom]
enabled = true
filter = http-custom
{% if fail2ban.webserver == "apache" -%}
logpath = /var/log/apache2/access.log
{% elif fail2ban.webserver == "nginx" -%}
logpath = /var/log/nginx/access.log
{% endif -%}
maxretry = {{ fail2ban.max_retries }}
findtime = 300
bantime = {{ fail2ban.ban_days * 86400 }}
banaction = iptables-allports
port = anyport

La task

Afin de copier les Templates Jinja2 sur le serveur distant, une task est nécessaire :
tasks/main.yml

- name: Create custom HTTP filter
  template:
    src: "{{ item }}.j2"
    dest: "/{{ item }}"
    owner: root
    group: root
    mode: '0644'
  loop:
    - etc/fail2ban/jail.d/http.conf
    - etc/fail2ban/filter.d/http-custom.conf
  notify: Restart fail2ban
  when: fail2ban.http is defined

Cette task s'exécutera uniquement si la liste définie dans notre fichier de variables n'est pas vide, et viendra relancer le service Fail2ban afin d'appliquer les changements.

Le Handler

Voici le Handler qui définit la relance du service précedemement énoncée.
handlers/main.yml

---
- name: Restart fail2ban
  service:
    name: fail2ban
    state: restarted

Voilà tout y est ! Vous pouvez maintenant utiliser ce rôle Ansible pour déployer des filtres en masse.