Desplegar Fail2ban con Docker Compose para Nginx Proxy Manager y SSH

Tiempo de lectura: 5 minutos

Hola, vamos a aprender cómo podemos desplegar Fail2ban. Fail2ban es una herramienta que protege los servidores de ataques de fuerza bruta mediante el bloqueo de direcciones IP que intentan acceder al servidor de forma repetida. En concreto vamos a implementar este contenedor: https://github.com/crazy-max/docker-fail2ban

Lo primero que tenemos que hacer es crear esta estructura de carpetas:

Ahora vamos a crear el archivo con la magia docker-compose.yml

version: "3.1"

services:
  fail2ban:
    image: crazymax/fail2ban:latest
    container_name: fail2ban
    network_mode: "host"
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - "./config/fail2ban/data:/data"
      - "./config/fail2ban/logs:/var/log:ro"
      - "./config/fail2ban/custom_configs:/etc/fail2ban/custom-config:ro"
      #Para sshd
      - ${PATH_DATA_LOGS_SSHD}:/var/data/sshd-logs
      #Para npm
      - ${PATH_DATA_LOGS_NPM}:/var/data/npm-logs
    env_file:
      - "./env/.fail2ban.env"
    restart: always

Ahora voy a explicar el docker compose:

Este código es un archivo de configuración de Docker Compose que define un servicio llamado «fail2ban».

En la sección «services», se define el servicio «fail2ban» que utiliza la imagen «crazymax/fail2ban:latest». Esta imagen contiene la herramienta Fail2ban y se descarga automáticamente desde Docker Hub.

El servicio se ejecuta en modo host, lo que significa que utiliza la red del host en lugar de una red aislada de Docker. Esto es necesario para que Fail2ban pueda acceder a los registros del sistema y bloquear las direcciones IP.

El servicio también tiene permisos de administrador de red y acceso a paquetes de red crudos. Esto es necesario para que Fail2ban pueda leer los registros del sistema y bloquear las direcciones IP.

En la sección «volumes», se definen varios volúmenes que se montan en el contenedor. Estos volúmenes se utilizan para almacenar los registros de sshd y npm, así como para almacenar los datos de configuración de Fail2ban.

En la sección «env_file», se carga el archivo de variables de entorno «.fail2ban.env» en el contenedor. Este archivo contiene variables de entorno que se utilizan para configurar Fail2ban.

Además ahora vamos a definir un .env para cargar las variables del contenedor Docker ${PATH_DATA_LOGS_SSHD} y ${PATH_DATA_LOGS_NPM}

Por último, en la sección «restart», se especifica que el servicio se reiniciará automáticamente en caso de fallo.

En resumen, este archivo de configuración de Docker Compose define un servicio de Fail2ban que se ejecuta en modo host y tiene acceso a los registros del sistema y permisos de administrador de red. También se definen varios volúmenes y se carga un archivo de variables de entorno para configurar Fail2ban.

Ahora vamos a crear el archivo .fail2ban.env

TZ=Europe/Paris

F2B_LOG_TARGET=STDOUT
F2B_LOG_LEVEL=INFO
F2B_DB_PURGE_AGE=1d

SSMTP_HOST=smtp.example.com
SSMTP_PORT=587
SSMTP_HOSTNAME=example.com
SSMTP_USER=smtp@example.com
SSMTP_PASSWORD=
SSMTP_TLS=YES

Recuerda indicar tus datos SSMTP (solo si quieres utilizar SSMT para recibir correos), por el momento no es necesario añadirlo.

Ahora nos creamos nuestro .env

#Logs de sistema:
PATH_DATA_LOGS_NPM=/data/logs
PATH_DATA_LOGS_SSHD=/var/logs

En el .env tenemos que localizar dos directorios:

  1. El directorio de NPM (Nginx Proxy Manager) PATH_DATA_LOGS_NPM (suele ser): /data/logs
  2. El directorio de SSH PATH_DATA_LOGS_SSHD (dónde tiene auth.log el sistema https://devcodelight.com/?p=6283&preview_id=6283&pre): /var/logs

Una vez configurado vamos a empezar a añadir las reglas:

Para SSH:

  • Añadimos en la carpeta config/fail2ban/data/jail.d el archivo sshd.conf
[sshd]
enabled = true
chain = INPUT
port = ssh
filter = sshd
logpath = /var/data/sshd-logs/auth.log
maxretry = 5

Podemos activar o desactivar con enabled. Aplicamos el filtro por defecto para ssh y el logpath está apuntando al directorio de docker-compose definido en el docker-compose.yml y el .env

Para Nginx Proxy manager (NPM) (esta configuración la reciclo de este tutorial (https://forum.openmediavault.org/index.php?thread/49480-nginx-proxy-manager-with-fail2ban-guide/):

  • Añadimos en la carpeta config/fail2ban/data/jail.d el archivo npm.conf
[npm-docker]
enabled = true
ignoreip = 127.0.0.1/8 #192.168.2.0/24 #Set ignored ranges
chain = INPUT
action = iptables-nft[type=allports, chain=DOCKER-USER]
logpath = /var/data/npm-logs/default-host_*.log
          /var/data/npm-logs/proxy-host-*.log
maxretry = 3
bantime  = 60
findtime = 300

Con esto indicamos que no tenga en cuenta las ips locales.

Aplicamos una acción que vamos a definir para que tenga en cuenta, llamada iptables-nft

Indicamos los logpath del Docker.

  • Ahora vamos a crear la accion, para ello añadimos en la carpeta config/fail2ban/data/filter.d el archivo npm-docker.conf
[INCLUDES]

[Definition]

failregex = ^<HOST>.+" (4\d\d|3\d\d) (\d\d\d|\d) .+$
            ^.+ 4\d\d \d\d\d - .+ \[Client <HOST>\] \[Length .+\] ".+" .+$

Aquí definimos el patrón dónde va a localizar errores e identificar la IP que debe bloquear.

  • Ahora vamos a crear la acción, para ello añadimos en la carpeta config/fail2ban/data/action.d el archivo iptables-nft.conf
## Version 2022/08/06iptables-nft
# Fail2Ban configuration file
#
# Authors: Sergey G. Brester (sebres), Cyril Jaquier, Daniel Black, 
#          Yaroslav O. Halchenko, Alexander Koeppe et al.
#

[Definition]

# Option:  type
# Notes.:  type of the action.
# Values:  [ oneport | multiport | allports ]  Default: oneport
#
type = oneport

# Option:  actionflush
# Notes.:  command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values:  CMD
#
actionflush = <iptables-nft> -F f2b-<name>

# Option:  actionstart
# Notes.:  command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values:  CMD
#
actionstart = { <iptables-nft> -C f2b-<name> -j <returntype> >/dev/null 2>&1; } || { <iptables-nft> -N f2b-<name> || true; <iptables-nft> -A f2b-<name> -j <returntype>; }
              <_ipt_add_rules>

# Option:  actionstop
# Notes.:  command executed at the stop of jail (or at the end of Fail2Ban)
# Values:  CMD
#
actionstop = <_ipt_del_rules>
             <actionflush>
             <iptables-nft> -X f2b-<name>

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
#
actioncheck = <_ipt_check_rules>

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionban = <iptables-nft> -I f2b-<name> 1 -s <ip> -j <blocktype>

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionunban = <iptables-nft> -D f2b-<name> -s <ip> -j <blocktype>

# Option:  pre-rule
# Notes.:  prefix parameter(s) inserted to the begin of rule. No default (empty)
#
pre-rule =

rule-jump = -j <_ipt_rule_target>

# Several capabilities used internaly:

_ipt_for_proto-iter = for proto in $(echo '<protocol>' | sed 's/,/ /g'); do
_ipt_for_proto-done = done

_ipt_add_rules = <_ipt_for_proto-iter>
              { %(_ipt_check_rule)s >/dev/null 2>&1; } || { <iptables-nft> -I <chain> %(_ipt_chain_rule)s; }
              <_ipt_for_proto-done>

_ipt_del_rules = <_ipt_for_proto-iter>
              <iptables-nft> -D <chain> %(_ipt_chain_rule)s
              <_ipt_for_proto-done>

_ipt_check_rules = <_ipt_for_proto-iter>
              %(_ipt_check_rule)s
              <_ipt_for_proto-done>

_ipt_chain_rule = <pre-rule><ipt_<type>/_chain_rule>
_ipt_check_rule = <iptables-nft> -C <chain> %(_ipt_chain_rule)s
_ipt_rule_target = f2b-<name>

[ipt_oneport]

_chain_rule = -p $proto --dport <port> <rule-jump>

[ipt_multiport]

_chain_rule = -p $proto -m multiport --dports <port> <rule-jump>

[ipt_allports]

_chain_rule = -p $proto <rule-jump>


[Init]

# Option:  chain
# Notes    specifies the iptables chain to which the Fail2Ban rules should be
#          added
# Values:  STRING  Default: INPUT
chain = INPUT

# Default name of the chain
#
name = default

# Option:  port
# Notes.:  specifies port to monitor
# Values:  [ NUM | STRING ]  Default:
#
port = ssh

# Option:  protocol
# Notes.:  internally used by config reader for interpolations.
# Values:  [ tcp | udp | icmp | all ] Default: tcp
#
protocol = tcp

# Option:  blocktype
# Note:    This is what the action does with rules. This can be any jump target
#          as per the iptables man page (section 8). Common values are DROP
#          REJECT, REJECT --reject-with icmp-port-unreachable
# Values:  STRING
blocktype = DROP

# Option:  returntype
# Note:    This is the default rule on "actionstart". This should be RETURN
#          in all (blocking) actions, except REJECT in allowing actions.
# Values:  STRING
returntype = RETURN

# Option:  lockingopt
# Notes.:  Option was introduced to iptables to prevent multiple instances from
#          running concurrently and causing irratic behavior.  -w was introduced
#          in iptables 1.4.20, so might be absent on older systems
#          See https://github.com/fail2ban/fail2ban/issues/1122
# Values:  STRING
lockingopt = -w

# Option:  iptables
# Notes.:  Actual command to be executed, including common to all calls options
# Values:  STRING
iptables-nft = iptables-nft <lockingopt>


[Init?family=inet6]

# Option:  blocktype (ipv6)
# Note:    This is what the action does with rules. This can be any jump target
#          as per the iptables man page (section 8). Common values are DROP
#          REJECT, REJECT --reject-with icmp6-port-unreachable
# Values:  STRING
blocktype = DROP

# Option:  iptables (ipv6)
# Notes.:  Actual command to be executed, including common to all calls options
# Values:  STRING
iptables-nft = ip6tables-nft <lockingopt>

Y la acción que va a realizar en este caso insertarlo en iptables para bloquear el acceso.

Ahora, una vez todo montado, tenemos que ejecutarlo poniendo:

docker compose up -d

Deja un comentario