Fail2ban est un outil formidable qui permet de surveiller les logs des différents services qui tournent sur vos serveurs, et en cas d’intrusions répétées de bannir l’adresse IP malicieuse. Nous avions vu lors d’un billet précédent comment se protéger des attaques bruteforce sur WordPress. Le problème de Fail2ban est que à chaque redémarrage du service ou du serveur, il repart à zéro : et du coup, vous perdez vos précieuses IPs bannies ; et vous serez à nouveau victime de plusieurs intrusions avant que Fail2ban repunisse les mécréants.
J’ai choisi une solution au niveau de l’applicatif : ce sera à Fail2ban de gérer cette rétention de bans. Une autre solution, plus générique, aurait été de gérer cela directement au niveau de iptables. Mais c’est beaucoup plus compliqué à gérer car il faut pouvoir distinguer les règles permanentes (par exemple autorisation du port SSH 22), des règles “temporaires” rajoutées par des applicatifs comme Fail2ban, mais aussi PortSentry… car oui, le fait qu’il y ait plusieurs applicatifs qui ajoutent/modifient/suppriment des règles de parefeu est encore un autre problème. Enfin, il faut savoir que Fail2ban DOIT connaitre ses règles afin d’éviter qu’il les duplique.
Bref, la solution est entièrement gérée par Fail2ban lui-même, aidé de trois scripts shell que je vous décris en détail ci-dessous.
Création de /usr/bin/fail2ban-save
Ce script récupéré de fail2ban.org et légèrement modifié va être exécuté avant l’arrêt du service Fail2ban. Il permet en fait de sauvegarder toutes les règles qu’il a ajouté dans iptables. Ce script affiche le code shell d’un script qui permet de réinjecter toutes les règles dans Fail2ban. Il suffit donc de rediriger sa sortie vers un fichier et d’exécuter ce dernier.
#!/bin/sh # Return fail2ban bans lists with fail2ban commands to reinject ban rules # Source : mehturt from http://www.fail2ban.org/wiki/index.php/Fail2ban:Community_Portal#Question_about_persistent_IP_address_bans_over_restart # Author : Arnaud Tanchoux / arnove.com / 2013-05-29 echo "#!/bin/sh" jails=$(fail2ban-client status | grep Jail\ list: | sed 's/.*Jail list:\t\+//;s/,//g') for jail in ${jails}; do for ip in $(fail2ban-client status ${jail}|grep IP\ list|sed 's/.*IP list:\t//'); do echo "fail2ban-client set ${jail} banip ${ip} > /dev/null" done done
Création préalable de /usr/bin/fail2ban-restore
La deuxième étape consiste à créer un fichier vide et à lui donner les permissions adéquates. En effet, le script vu précédemment va écrire les règles de Fail2ban dans ce fichier. Et nous aurons besoin de l’exécuter plus tard.
touch /usr/bin/fail2ban-restore chmod +x /usr/bin/fail2ban-restore
Modification du fichier /etc/init.d/fail2ban
Enfin, nous devons modifier le script de lancement/arrêt de Fail2ban. Il y a très peu de modifications (voir les lignes surlignées) : elles consistent juste à appeller /usr/bin/fail2ban-save avant d’arrêter Fail2ban, et d’appeller /usr/bin/fail2ban-restore après avoir démarré Fail2ban.
Le script /etc/init.d/fail2ban ci-dessous est celui fourni par Fail2ban pour Debian. N’hésitez pas à l’adapter à votre serveur/distribution : seules deux lignes vraiment nécessaire doivent être ajoutées. Le reste n’est que présentation et commentaires.
#! /bin/sh ### BEGIN INIT INFO # Provides: fail2ban # Required-Start: $local_fs $remote_fs # Required-Stop: $local_fs $remote_fs # Should-Start: $time $network $syslog iptables firehol shorewall ipmasq arno-iptables-firewall # Should-Stop: $network $syslog iptables firehol shorewall ipmasq arno-iptables-firewall # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start/stop fail2ban # Description: Start/stop fail2ban, a daemon scanning the log files and # banning potential attackers. ### END INIT INFO # Author: Aaron Isotton <aaron@isotton.com> # Modified: by Yaroslav Halchenko <debian@onerussian.com> # reindented + minor corrections + to work on sarge without modifications # Modified: by Arnaud Tanchoux <noc@arnove.net> for arnove.com # save currently banned ips on stop, and reinject those after start # PATH=/usr/sbin:/usr/bin:/sbin:/bin DESC="authentication failure monitor" NAME=fail2ban # fail2ban-client is not a daemon itself but starts a daemon and # loads its with configuration DAEMON=/usr/bin/$NAME-client SCRIPTNAME=/etc/init.d/$NAME # Ad-hoc way to parse out socket file name SOCKFILE=`grep -h '^[^#]*socket *=' /etc/$NAME/$NAME.conf /etc/$NAME/$NAME.local 2>/dev/null \ | tail -n 1 | sed -e 's/.*socket *= *//g' -e 's/ *$//g'` [ -z "$SOCKFILE" ] && SOCKFILE='/tmp/fail2ban.sock' # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME DAEMON_ARGS="$FAIL2BAN_OPTS" # Load the VERBOSE setting and other rcS variables [ -f /etc/default/rcS ] && . /etc/default/rcS # Predefine what can be missing from lsb source later on -- necessary to run # on sarge. Just present it in a bit more compact way from what was shipped log_daemon_msg () { [ -z "$1" ] && return 1 echo -n "$1:" [ -z "$2" ] || echo -n " $2" } # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. # Actually has to (>=2.0-7) present in sarge. log_daemon_msg is predefined # so we must be ok . /lib/lsb/init-functions # # Shortcut function for abnormal init script interruption # report_bug() { echo $* echo "Please submit a bug report to Debian BTS (reportbug fail2ban)" exit 1 } # # Helper function to check if socket is present, which is often left after # abnormal exit of fail2ban and needs to be removed # check_socket() { # Return # 0 if socket is present and readable # 1 if socket file is not present # 2 if socket file is present but not readable # 3 if socket file is present but is not a socket [ -e "$SOCKFILE" ] || return 1 [ -r "$SOCKFILE" ] || return 2 [ -S "$SOCKFILE" ] || return 3 return 0 } # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started do_status && return 1 if [ -e "$SOCKFILE" ]; then log_failure_msg "Socket file $SOCKFILE is present" [ "$1" = "force-start" ] \ && log_success_msg "Starting anyway as requested" \ || return 2 DAEMON_ARGS="$DAEMON_ARGS -x" fi # Assure that /var/run/fail2ban exists [ -d /var/run/fail2ban ] || mkdir -p /var/run/fail2ban start-stop-daemon --start --quiet --chuid root --exec $DAEMON -- \ $DAEMON_ARGS start > /dev/null\ || return 2 # Restore previously banned IPs saved by /usr/bin/fail2ban-save /usr/bin/fail2ban-restore return 0 } # # Function that checks the status of fail2ban and returns # corresponding code # do_status() { $DAEMON ping > /dev/null return $? } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred # Check fail2ban daemon status $DAEMON status > /dev/null || return 1 # Save currently banned IPs to reinject those on fail2ban daemon next start /usr/bin/fail2ban-save > /usr/bin/fail2ban-restore # Stop fail2ban daemon $DAEMON stop > /dev/null || return 2 # now we need actually to wait a bit since it might take time # for server to react on client's stop request. Especially # important for restart command on slow boxes count=1 while do_status && [ $count -lt 60 ]; do sleep 1 count=$(($count+1)) done [ $count -lt 60 ] || return 3 # failed to stop return 0 } # # Function to reload configuration # do_reload() { $DAEMON reload > /dev/null && return 0 || return 1 return 0 } # yoh: # shortcut function to don't duplicate case statements and to don't use # bashisms (arrays). Fixes #368218 # log_end_msg_wrapper() { [ $1 -lt $2 ] && value=0 || value=1 log_end_msg $value } command="$1" case "$command" in start|force-start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start "$command" [ "$VERBOSE" != no ] && log_end_msg_wrapper $? 2 ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop [ "$VERBOSE" != no ] && log_end_msg_wrapper $? 2 ;; restart|force-reload) log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start log_end_msg_wrapper $? 1 ;; *) # Failed to stop log_end_msg 1 ;; esac ;; reload|force-reload) log_daemon_msg "Reloading $DESC" "$NAME" do_reload log_end_msg $? ;; status) log_daemon_msg "Status of $DESC" do_status case $? in 0) log_success_msg " $NAME is running" ;; 255) check_socket case $? in 1) log_warning_msg " $NAME is not running" ;; 0) log_failure_msg " $NAME is not running but $SOCKFILE exists" ;; 2) log_failure_msg " $SOCKFILE not readable, status of $NAME is unknown";; 3) log_failure_msg " $SOCKFILE exists but not a socket, status of $NAME is unknown";; *) report_bug "Unknown return code from $NAME:check_socket.";; esac ;; *) report_bug "Unknown $NAME status code" esac ;; *) echo "Usage: $SCRIPTNAME {start|force-start|stop|restart|force-reload|status}" >&2 exit 3 ;; esac :
Et voilà, ce n’est pas plus compliqué que cela. Désormais, à chaque redémarrage de Fail2ban, vos règles seront automatiquement réinjectées. La encore, si vous avez activé les notifications par E-Mail, en plus de recevoir les messages de notifications de stop et de start de chaque jail, vous recevrez également les notifications de ban des IPs sauvegardées. Cela dépend du moins de la façon dont vous avez configuré votre Fail2ban.