Votre intervenant informatique sur la presqu'île de Quiberon, et au-delà

Si vous avez des sites webs à gérer, vous aurez constaté, à la lecture de vos fichiers de log (Apache access.log par exemple) que les visiteurs qui y laissent des traces ne sont pas tous des humains. Beaucoup de ces robots, web spiders et autres y sont pour des raisons légitimes (ce sont par exemple les robots d’indexation des moteurs de recherche), pour d’autre vous vous demandez ce qu’ils viennent faire là.

J’ai décidé de m’attaquer au problème avec l’idée suivante en tête. Vérifier chaque adresse ip dans le log pour savoir si on pouvait ou pas lui autoriser l’accès au site.

Fail2ban

Le programme fail2ban, disponible sur de nombreuses distributions Linux, lit les logs et, en fonction de règles définies par les utilisateurs, peut y détecter des signatures de comportements malveillants et bannir pour un temps les adresses IP malveillantes. Dans notre cas, il est difficile de savoir si une IP est malveillante ou pas. C’est pour ça qu’il est nécessaire de recourir à un service externe.

Apility.io

Apility.io fournit un tel service. Une inscription gratuite permet d’obtenir une clef d’API qui autorise une connexion à sa base de donnée. Apility.io propose de composer soit même sa « blacklist » en choisissant différentes sources d’IP plus ou moins malveillantes avec différents critères. Par exemple une base regroupe les IPs hébergées dans un data-center. Ce n’est pas en soit un signe de malveillance, même si le nombre d’utilisateurs humains qui se cachent derrière doit être plutôt réduit. De la même manière, les IP de sorties des VPN sont regroupées dans des blacklists distinctes. En soit, on peut utiliser un VPN pour des raisons tout à fait légitimes. Il faut donc faire un choix des blacklists que l’on veut considérer.

Perl et SQLite

Nous appellerons Apility.io par son API. Il faut noter que quand un utilisateur consulte un site web, il fait de nombreuses requêtes distinctes au serveur web. Nous n’allons pas consulter Apility.io pour chaque entrée dans le access.log, mais une fois seulement, et conserveront le résultat (IP bienveillante ou malveillante) dans une base de donnée SQLite, le tout enrobé dans un programme en Perl.

Tout les éléments sont donc réunis pour bâtir ce système de bannissement des robots indésirables. Il fonctionne de la manière suivante:

  • Fail2ban scanne le fichier access.log du site web pour y récupérer les adresses IP des visiteurs
  • Quand une IP est détectée, le programme en perl banbadip.pl est exécuté. Si l’adresse n’est pas présente dans la base de donnée, le programme fait un appel au web service de Apility.io pour lui demander ce qu’il en pense. Le résultat est transformé en une réponse OK/BAD et stocké dans la base de données. Si la réponse était déjà dans la base de données, c’est la réponse stockée qui est servie. Les données restent stockées un temps limité dans la base de donnée pour avoir les données Apility.io les plus à jour.
  • Si l’IP est considérée valide, on dit à fail2ban de ne pas s’en occuper. Sinon l’IP est bannie pendant un temps défini.

Travaux pratiques

Fail2ban doit être installé et configuré. Ensuite dans le fichier /etc/fail2ban/jail.conf ou /etc/fail2ban/jail.conf vous devez définir une « jail » (prison). Ce sont les règles de bannissement pour un service en particulier. Pour notre cas particulier ce sera:

[myipban]
port = http,https
logpath = /var/log/apache2/access.log
bantime = 3600
findtime = 2
maxretry = 1
filter = myipban
enabled=true
ignorecommand = perl /root/banbadip.pl --db=/root/badipdb.sqlite \
     --action=check --ip=<ip> 
banaction = myipbanaction

On y décrit le fichier de log qui est traité, la durée du bannissement, maxretry = 1 signifie que les IP sont traitées dès qu’elles sont vues une fois, enabled = true pour activer cette règle, filter = myipban est le nom du filtre qui identifie les lignes à traiter dans le fichier log, banaction est le nom de l’action à effectuer quand une IP est à bannir, ignorecommand est la commande à exécuter pour déterminer si l’IP est considérée comme à bannir ou pas.

Le filtre myipban est décrit dans un fichier myipban.conf dans le répertoire /etc/fail2ban/filter.d/ il contient des définitions qui servent à reconnaître l’IP et la date à partir du fichier de log.

[INCLUDES]
before = common.conf
[Definition]
datepattern = ^[^\[]*\[({DATE}) 
               {^LN-BEG}
failregex = ^<HOST> -.*

L’action est décrite dans un fichier myipbanaction.conf dans le répertoire /etc/fail2ban/action.d/. Elle contient les actions à faire pour bannir et (dé)bannir une adresse IP. Par exemple, si votre système utilise iptables pour contrôler les accès:

[INCLUDES]
before = iptables-common.conf

[Definition]
actionstart = iptables -N f2b-myipban
              iptables -A f2b-myipban -j RETURN
	      iptables -I INPUT -p tcp --dport https -j f2b-myipban
      	      iptables -I INPUT -p tcp --dport http -j f2b-myipban	
actionstop = iptables -D INPUT -p tcp --dport http -j f2b-myipban
             iptables -D INPUT -p tcp --dport https -j f2b-myipban
	     iptables -F f2b-myipban
             iptables -X f2b-myipban
actioncheck = 
actionban = iptables -I f2b-myipban 1 -s <ip> -j REJECT
actionunban = iptables -D f2b-myipban -s <ip> -j REJECT 

Il reste à vous montrer un exemple de code pour détecter si l’adresse est à bannir ou pas. Vous trouverez ce code dans la suite. Il faudra remplacer YOUR-API-KEY par votre clef api récupérée sur votre compte apility.io .

Le script devra être copié dans le répertoire /root (ou ailleurs mais il faudra alors modifier le fichier jail.local) et il utilise une base de données sqlite située dans le même répertoire. Vous pouvez changer tout ça. C’est un script Perl qui utilise un certain nombre de modules que vous devrez probablement installer.

#!/usr/bin/env perl
use Getopt::Long ;
use LWP ;
use LWP::UserAgent;
use DBI ;
use Data::Dumper ; 
use JSON;
use List::Compare ;
##
# This script is run by fail2ban with an ip as entry
##
GetOptions("ip=s" => \$ip ,
	   "db=s" => \$db ,
	   "action=s" => \$action ,
	   "ban" => \$ban,
	   "strict" => \$strict
) or die("Error in running command");




die("database not defined") if(! defined($db)) ;
die("ip not defined") if(! defined($ip)) ;

my $dbh ;

if( ! -f $db ) {
    # Create database if does not exist
    $dbh = DBI->connect("dbi:SQLite:dbname=$db","","");
    my $create = "create table ips( ip text , ts text default CURRENT_TIMESTAMP , status text );" ;
    my $sth = $dbh->prepare($create);
    $sth->execute;
} else {
    # connect to database
    $dbh = DBI->connect("dbi:SQLite:dbname=$db","","");
}


if( $action eq "check"){
    # purge database
    deleteOldentries();

    # search in database 
    my $query = "select status from ips where ip = ?" ;
    my $stp = $dbh->prepare($query);
    $stp->execute($ip);
    my $ref = $stp->fetchall_arrayref;
    my @rows = @$ref ;
    my $nrow = $#rows + 1  ;
#    print $nrow ."\n" ;
#    print Dumper $ref->[0] ;
    if( $nrow == 1 ){ #found in database
	# using the cached value 
	my $row = @$rows[0] ;
	$status = $ref->[0]->[0] ;
    } else {
	# not found in database: calling apility
	$res =  checkip($ip) ;
	if( $res =~ /^404 /){
	    $status = "OK" ;
	} elsif($res =~ /^400 /){
	    $status = "OK" ;
	} else {
	    my $out = decode_json $res ;

	    if( defined( $strict )){
		@valid = () ;
	    } else {
		@valid = ("IPCATV4-DC","NORDVPN-EXIT-IP","EXPRESSVPN-EXIT-IP") ;
	    }
	    my @blacklists = @{$out->{"response"}} ;
	    
	    $lc = List::Compare->new('-u', \@blacklists, \@valid);
	    my @tot = $lc->get_Lonly ;
	    foreach my $item1 (@blacklist){
		foreach my $item2 (@valid){
		}
	    }
	    if($#tot == 0 ){ 
		$status = "BAD" ;
	    } else {
		$status = "OK" ;
	    }
	}
	# save in database
	my $insert = "insert into ips(ip,status) values(?,?)" ;
	my $stp = $dbh->prepare($insert);
	$stp->execute($ip,$status);
    }
    if( $status eq "BAD" ){
	exit(1) ;
    } else {
	exit(0) ;
	# do nothing if status ok
    }
    
}

sub getStatusFromResponse{
}

sub deleteOldentries{
    # delete old entries from the database
    my $delete = "delete from ips where ts < datetime('now', '-1 hour')" ;
    my $stp = $dbh->prepare($delete);
    $stp->execute() ;
}

sub checkip{
    my ($iptest) = @_ ;
    my $ua = LWP::UserAgent->new;
    $ua->agent("MyApp/0.1 ");
    
    my $req = HTTP::Request->new(GET => "https://api.apility.net/badip/$iptest");
    $req->header('X-Auth-Token'=> 'YOUR-API-KEY',
		 'Accept' => 'application/json' );
    my $res = $ua->request($req);
    if ($res->is_success) {
	return $res->content;
    } else {
	return $res->status_line ;
    }
}

Vous pouvez à tout moment regarder le contenu de la base de données sqlite (qui est une base de données sql contenue dans un fichier plat) et compter le nombre d’IP OK ou BAD

sqlite3 badipdb.sqlite "select * from ips where status = 'OK'"  | wc -l
sqlite3 badipdb.sqlite "select * from ips where status = 'BAD'"  | wc -l

Sur deux sites recevant plusieurs milliers de visiteurs par jour, de 10 à 20% des visiteurs sont identifiés comme étant des visiteurs malveillants et sont tenus à l’écart.

Contactez moi pour plus d’information.