angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Mettre en liste noire les adresses IP sur votre site web Flask fonctionnant sur Linux

16 avril 2020 à côté de Peter
Dans Flask

Parfois, vous souhaitez bloquer les adresses IP immédiatement. Ce post décrit une méthode pour y parvenir.

post main image
https://unsplash.com/@vladbahara

Vous avez un site web et il fonctionne bien. Mais vous remarquez que certains visiteurs essaient de vous embrouiller avec vos formulaires. Ils proviennent d'adresses IP spécifiques. Ensuite, il y a aussi des robots qui scannent votre site. Certains sont nécessaires, mais d'autres doivent rester à l'écart. Vous ne détestez pas cela ? Si, je déteste ça. Dans le passé, j'ai écrit un module qui renvoyait une réponse pas très agréable très lentement, octet par octet, ralentissant leur système. Ou qui renvoyait une quantité interminable de données. Mais c'est une autre histoire.

Pour l'instant, je veux me concentrer sur une autre méthode : le blocage de ces demandes. Il suffit de renvoyer un HTTP 403 Forbidden. Je veux pouvoir le faire à la volée depuis la section d'administration de mon site web. Nous y spécifions les adresses IP ou la plage d'adresses IP que nous voulons bloquer. Il y a aussi d'autres moyens de le faire, comme l'utilisation des fichiers .htaccess et les paramètres du serveur web. Je les mentionnerai au bas de ce billet.

Plusieurs raisons de bloquer

J'ai déjà mentionné que l'une des raisons de bloquer l'accès à votre site est de bloquer les visiteurs malveillants. Ils veulent voir comment ils peuvent casser votre site, remplir votre section de commentaires avec de la publicité ou d'autres messages fous. Il y a de nombreuses raisons pour lesquelles cela est fait par je crois que l'une d'entre elles est qu'ils veulent vous forcer à prendre un plugin anti-spam tiers. Ceux-ci peuvent être très efficaces car ils se connectent à d'énormes bases de données contenant des informations sur le spam. Mais si nous voulons respecter la vie privée de nos visiteurs, nous ne pouvons pas utiliser un tel plugin. Nous devons utiliser d'autres moyens, et un dernier recours est souvent le blocage des adresses IP.

Il peut également être nécessaire de bloquer certains robots qui scannent votre site. Certains bots génèrent un volume de trafic considérable. J'ai vérifié tout le trafic de ce site pendant une certaine période et il est apparu que seulement 10%, probablement même moins, des demandes provenaient de vrais visiteurs ! Bien sûr, tous les bots ne sont pas mauvais, mais certains ne respectent vraiment pas les règles. La plupart des bots peuvent être identifiés par la chaîne User Agent. J'ai trouvé les deux suivants que je veux vraiment bloquer :

  • SemrushBot
  • AhrefsBot

Faites attention à ce que vous devez bloquer, de nombreux robots sont utilisés pour faire apparaître votre site dans les résultats des moteurs de recherche. SemrushBot est à propos de SEO, je ne l'utilise pas pour le moment. Le blocage des agents utilisateurs n'est pas abordé dans ce post. Il ne changera pas aussi souvent et vous pouvez bloquer d'autres façons.

Les bonnes choses des demandes non désirées

Si vous mettez en place un système d'enregistrement approprié, vous pouvez également tirer parti des demandes indésirables. La liste ci-dessous présente certaines demandes qui ont provoqué une erreur HTTP 404 pour ce site :

http://peterspython.com/css/album.css
http://www.peterspython.com/wordpress
http://peterspython.com/blog/wp-includes/wlwmanifest.xml
http://peterspython.com/wordpress/wp-includes/wlwmanifest.xml
http://peterspython.com/website/wp-includes/wlwmanifest.xml
http://peterspython.com/public/ui/v1/js/sea.js
http://www.peterspython.com/public/ui/v1/js/sea.js
http://peterspython.com/vendor/phpunit/phpunit/phpunit.xsd
http://peterspython.com/vendor/phpunit/phpunit/LICENSE
http://www.peterspython.com/apple-touch-icon.png
http://peterspython.com/humans.txt
http://peterspython.com/license.txt

Nous voyons que les robots recherchent le fichier wlwmanifest.xml. Il semble que ce soit un fichier associé à "Windows Live Writer", une application de publication de blog développée par Microsoft qui a été arrêtée en 2017 et qui pourrait être vulnérable. Une autre attaque est à la recherche de " PHPUnit", une unité de test de " PHP ". Il contenait une vulnérabilité qui n'a peut-être pas encore été corrigée. D'autres bots d'attaque peuvent générer des URL qui provoquent une erreur HTTP 500. Cette erreur peut être intentionnelle, mais peut également être causée par des faiblesses de votre site.

La bonne nouvelle, c'est que vous pouvez utiliser ces informations pour améliorer votre site. Veillez toujours à mettre en place une journalisation correcte, les erreurs vous donnent des informations très précieuses !

Limitez-vous aux adresses IP IPv4 uniquement

Le blocage des visiteurs par leur adresse IP a ses limites. De nombreuses personnes sur Internet obtiennent leur adresse IP lorsqu'elles se connectent à un serveur en utilisant DHCP. C'est surtout vrai pour les téléphones portables. Faites donc attention à ce qu'il faut bloquer.

Il y a aussi IPv6 qui a été conçu pour surmonter la disponibilité limitée des adresses IPv4 . Bien que certains rapports indiquent que 30 % du trafic internet se fait sur IPv6, le nombre de serveurs ayant effectivement activé IPv6 est bien inférieur. Cela signifie heureusement qu'il n'y a aucune raison de migrer votre serveur vers IPv6 pour le moment. Bloquer le spam avec IPv6 est possible avec cette méthode mais il y a un "gotcha".

Opérations administratives et règles relatives aux adresses IP sur liste noire

Dans l'administrateur, je veux spécifier les adresses IP que je veux mettre sur liste noire. Il existe un tableau avec les enregistrements d'adresses IP sur liste noire. Pour les adresses IP, je veux pouvoir spécifier les adresses IP comme suit :

  1. Une seule adresse IP, par exemple : 1.2.3.4
  2. Un réseau IP, exemple : 1.2.3.0/24
  3. Une plage d'adresses IP, par exemple : 1.2.3.6-1.2.4.2

Je spécifie une de ces adresses dans un seul enregistrement et j'appelle cela une "règle d'adresse IP sur liste noire".

Mise en cache pour éviter l'accès à la base de données

Nous ne voulons certainement pas accéder à la base de données à chaque demande pour voir si la demande est autorisée. Cela ralentirait les demandes. C'est pourquoi nous utilisons la mise en cache. Au lieu d'interroger la base de données, nous vérifions d'abord le cache pour voir si l'adresse IP a déjà accédé au site. Pour chaque adresse IP, nous avons un indicateur appelé "autorisé". Si l'adresse IP est vraie, l'accès est autorisé ; si elle est fausse, l'accès est bloqué.

Si l'adresse IP se trouve dans le cache, nous avons terminé, nous continuons ou nous bloquons. Si l'adresse IP n'est pas dans le cache, nous vérifions si elle figure dans la liste noire des règles relatives aux adresses IP. Le résultat est ajouté au cache, et la prochaine fois qu'une requête avec cette adresse IP arrive sur notre site, les données sont dans le cache et la base de données ne doit pas être interrogée.

Ajout et suppression des règles relatives aux adresses IP sur liste noire

Supposons que nous ayons des centaines, des milliers d'objets dans notre cache. Nous voulons maintenant apporter des modifications en utilisant l'administrateur, soit en ajoutant une règle d'adresse IP sur liste noire, soit en supprimant une règle d'adresse IP sur liste noire.

L'ajout ou la suppression d'une règle n'est pas trivial car la règle peut inclure des adresses IP qui sont déjà dans le cache. Que faire des valeurs mises en cache ? La façon la plus simple est de vider le cache et de le laisser se reconstruire. Cela ralentira les prochaines requêtes pendant un court laps de temps. Le seul autre moyen est de scanner les adresses IP dans le cache et de vérifier si elles correspondent à la règle d'ajout ou de suppression des adresses IP sur liste noire. Si elles correspondent, nous les supprimons du cache. J'ai quelques idées sur la manière de mettre en œuvre ce système, mais je ne l'ai pas encore fait.

Ajout de l'horodatage

Pour une performance maximale, les informations relatives aux adresses IP dans le cache sont en lecture seule et n'expirent pas. Cela signifie qu'elles peuvent devenir énormes au fil du temps si vous avez beaucoup de visiteurs. Comme la plupart des visiteurs n'accèdent à votre site que pendant quelques minutes, nous pouvons ajouter un horodatage aux adresses IP mises en cache, qui est mis à jour à chaque accès. L'horodatage permet de supprimer facilement les anciennes entrées.

Demandes au même moment

Supposons que deux demandes, la demande A et la demande B, arrivent en même temps, toutes deux utilisant la même adresse IP. Si elles ne sont pas en cache, toutes deux vérifieront si leur adresse IP est bloquée en consultant le tableau des règles de la liste noire des adresses IP. Ensuite, elles mettent toutes deux à jour l'élément cached_access. La demande A crée d'abord l'élément autorisé. Mais ensuite, la demande B crée l'élément autorisé, en écrasant l'élément autorisé de la demande A. Il en va de même si l'on veut mettre à jour l'horodatage de l'élément mis en cache. Cela peut sembler mauvais, mais ce n'est pas si grave. Nous devons juste nous assurer que l'opération de création est atomique.

Utiliser le système de fichiers Linux comme cache

Pour l'instant, je choisis d'implémenter le cache avec des fichiers. Le système de fichiers Linux est assez rapide pour gérer cela pour ma demande. Je ne veux pas ajouter quelque chose comme Redis, je veux garder les dépendances minimales.

Si nous avons un fichier "autorisé" par adresse IP, alors le fichier peut être petit, le contenu est 0 (bloqué) ou 1 (autorisé). Pour éviter un nombre important de fichiers dans un répertoire et ralentir la recherche, nous créons des sous-répertoires basés sur l'adresse IP. Nous divisons l'adresse IP par le point ('.') et l'utilisons pour créer des répertoires. L'horodatage du fichier "autorisé" change automatiquement lors de la lecture du fichier. Dans Linux , nous avons les horodatages suivants :

  • mtime (ls -l)
    La dernière fois que le contenu du fichier a été modifié
  • ctime
    La dernière fois que l'état du fichier, par exemple les autorisations, a changé
  • atime (ls -lu)
    La dernière fois que le fichier a été lu

Nous pouvons utiliser l'heure comme horodatage. Nous n'avons pas besoin de mettre à jour l'heure du fichier. Il y a un problème si vous voulez pouvoir afficher le contenu des fichiers autorisés dans l'administration. Cela permettrait de lire les fichiers et de modifier les horodatages. Nous pouvons résoudre ce problème en créant une copie du fichier "autorisé". La lecture de la copie ne modifie pas l'heure du fichier "autorisé" original.

Un avertissement lors de l'utilisation du temps d'accès Linux atime

Il existe de nombreuses informations sur Internet concernant les horodatages Linux , mais très peu d'entre elles mentionnent que cela pourrait ne pas fonctionner comme prévu. Je vous invite à consulter les liens ci-dessous à ce sujet. Par exemple, vous pouvez vérifier si relatime est une option de montage avec cette commande :

cat /proc/mounts | grep relatime

Le résumé est :

  • La mise à jour de l'horodatage à chaque lecture est désactivée par défaut pour des raisons de performances
  • Depuis le noyau 2.6.30, relatime est l'option par défaut
  • Depuis le noyau 2.6.30, la dernière heure d'accès à un fichier est toujours mise à jour si elle date de plus d'un jour

Cela signifie que nous pouvons toujours utiliser atime mais que nous devons respecter une résolution d'un jour. Pas de problème pour moi mais attendez, testons si cela fonctionne vraiment.

ls -l

Le résultat est le suivant :

total 8
-rw-r--r-- 1 flaskuser  flaskgroup 1 apr 16 15:47 allowed
-rw-r--r-- 1 flaskuser  flaskgroup 1 apr 16 15:47 allowed_copy

Ensuite, nous voulons voir l'heure, ou le temps d'accès, du fichier autorisé :

stat allowed

Le résultat est :

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:47:06.024559817  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:47:06.024559817  +0200
 Birth: -

Nous modifions alors l'heure d'accès à la veille :

sudo touch -a -t 202004151530.02 allowed

Le résultat de la commande stat montre que le temps d'accès est un jour plus tôt :

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-15 15:30:02.000000000  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:52:12.472562630  +0200
 Birth: -

Nous générons maintenant une demande sur le site et après la demande, nous exécutons à nouveau la commande stat :

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:56:24.200564941  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:52:12.472562630  +0200
 Birth: -

Le temps d'accès a été actualisé à ce jour. Les demandes ultérieures ne mettent plus à jour le temps d'accès. En travaillant comme prévu, j'ai appris quelque chose aujourd'hui.

Détails de l'implémentation

J'ai appelé la classe CachedAccess. Dans le Flask de before_request , je l'instancie comme suit :

    @app.before_request
    def  before_request():
        ...
        g.ip_address = get_ip_address()
        ...
        cached_access = CachedAccess()
        if not cached_access.is_allowed():
            # bye bye
            abort(403)

Et voici les parties (importantes) de la classe :

class CachedAccess:

    def __init__(self):
        ...


    def log_block(self, reason):
        ...


    def is_allowed_ip_address(self, ip_address_uint):

        # check: single ip addresses
        access_block_ip_address = db_select(
            model_class_list=[AccessBlockIPAddress],
            filter_by_list=[
                (AccessBlockIPAddress, 'is_active', 'eq', True),
                (AccessBlockIPAddress, 'ip_address_type', 'eq', 3),
                (AccessBlockIPAddress, 'ip_address_uint', 'eq', ip_address_uint),
            ],
        ).first()

        if access_block_ip_address is not  None:
            # found
            return False

        # check: network and range
        access_block_ip_address = db_select(
            model_class_list=[AccessBlockIPAddress],
            filter_by_list=[
                (AccessBlockIPAddress, 'is_active', 'eq', True),
                (AccessBlockIPAddress, 'ip_address_type', 'in', [1, 2]),
                (AccessBlockIPAddress, 'ip_address_from_uint', 'le', ip_address_uint),
                (AccessBlockIPAddress, 'ip_address_to_uint', 'ge', ip_address_uint),
            ],
        ).first()

        if access_block_ip_address is not  None:
            # found
            return False

        return True


    def is_allowed(self):

        # check if valid ip_address
        try:
            ip_address_uint = int( ipaddress.ip_address(g.ip_address) )
        except Exception as e:
             current_app.logger.error(fname  +  ': not a valid ip address = {}, {}'.format(g.ip_address, str(e)))
            return True

        # create ip_address_file
        app_cached_access_dir =  current_app.config['APP_CACHED_ACCESS_DIR']
        ip_address_parts = g.ip_address.split('.')
        ip_address_file = os.path.join(app_cached_access_dir, *ip_address_parts, 'allowed')

        # check if file exists and read its contents
        found = True
        try:
            with open(ip_address_file, 'r') as f:
                allowed = f.read()
        except:
               found = False

        if found:
            # done
            if allowed == '1':
                return True
            self.log_block(1)
            return False

        # check if g.ip_address matches a rule in blacklisted IP addresses table
        allowed = self.is_allowed_ip_address(ip_address_uint)

        # create directories for g.ip_address
        ip_address_dir = os.path.dirname(ip_address_file)
        try:
            pathlib.Path(ip_address_dir).mkdir(parents=True, exist_ok=True)
        except Exception as e:
             current_app.logger.error(fname  +  ': error creating directories ip_address_dir = {}, {}'.format(ip_address_dir, str(e)))
            return True

        # create allowed temp file
        temp_name = next(tempfile._get_candidate_names())
        ip_address_temp_file = os.path.join(app_cached_access_dir, *ip_address_parts, temp_name)
        try:
            with open(ip_address_temp_file, 'w') as f:
                f.write( '1' if allowed else '0' )
        except Exception as e:
             current_app.logger.error(fname  +  ': error writing ip_address_temp_file = {}, {}'.format(ip_address_temp_file, str(e)))
            return True

        # atomic move ip_address_temp_file to ip_address_file
        try:
            os.rename(ip_address_temp_file, ip_address_file)
        except Exception as e:
             current_app.logger.error(fname  +  ': error renaming ip_address_temp_file = {} to  ip_address_temp_file = {}, {}'.format(ip_address_temp_file, ip_address_temp_file, str(e)))
            return True

        if allowed:
            return True

        self.log_block(2)
        return False

Ce n'est pas vraiment très difficile. Je convertis l'adresse IP en un Int non signé afin que nous puissions vérifier si elle se trouve dans un réseau IP ou une plage d'adresses IP. Si une erreur inattendue se produit, j'enregistre l'erreur et j'autorise l'adresse IP. Cela signifie que nous ne bloquons pas les demandes inattendues.

Développement et production

Lors du développement, vous verrez probablement de nombreuses requêtes bloquées pendant les tests. La raison en est que les images, les fichiers Javascript , etc. sont également desservis par le serveur de développement Flask . Vous pouvez filtrer ces requêtes dans votre code :

    if request_path.startswith( ('/static/') ):
        return

En production, je suppose que vous diffusez tout votre contenu statique par le serveur web, Nginx, Apache, ce qui signifie que vous ne perdez pas de temps. Nous ne faisons que bloquer les demandes au code, les images etc. ne sont pas bloquées.

Blocage avec Nginx

Je ne voulais pas contrôler mon serveur web Nginx pour rester simple. Mais il n'est pas si difficile de lui dire de bloquer les requêtes. Si vous utilisez Nginx, vous pouvez ajouter quelques lignes pour bloquer plusieurs agents user comme suit :

    if ($http_user_agent ~* (wget|curl|libwww-perl) ) {
        return 403;
    }

Et pour bloquer plusieurs adresses IP que vous pouvez utiliser :

    location / {
        deny 127.0.0.1; # Individual IP Address
        deny 1.2.3.0/24; # IP network
    }

Mais ce n'est pas ce que nous voulons. Nous voulons un blocage dynamique. Il existe plusieurs moyens d'y parvenir, mais vous devrez bien sûr vous impliquer beaucoup plus dans les spécificités de Nginx . Il y a suffisamment d'exemples sur Internet pour illustrer la manière de procéder.

Résumé

Je voulais vraiment mettre en place une liste noire d'adresses IP à la volée et cela n'a pas semblé si difficile. Je n'ai pas tout mis en œuvre pour le moment. Cela signifie qu'il n'y a pas de mise à jour intelligente après l'ajout ou la suppression des règles de la liste noire d'adresses IP. Au lieu de cela, j'ai un bouton "flush cache" que je peux actionner après avoir apporté des modifications au tableau de la liste noire. C'est comme un "rm -R" dans Python.

L'horodatage du temps d'accès Linux m'a retardé dans la rédaction de ce billet, je n'ai jamais utilisé le temps d'accès mais maintenant je connais ses particularités. Linux met à jour le temps d'accès une fois par jour, cela me convient.

Je doute que vous puissiez obtenir de meilleures performances, mais vous pourriez envisager d'autres options comme la mise en cache de l'élément en mémoire. Vous pourriez utiliser le TTLCache de l'outil de cache Python .

Liens / crédits

cachetools
https://pypi.org/project/cachetools/

Dynamic Blacklisting of IP Addresses
https://docs.nginx.com/nginx/admin-guide/security-controls/blacklisting-ip-addresses/

flask-ipban
https://github.com/Martlark/flask-ipban

flask-ipblock
https://github.com/closeio/flask-ipblock

flask-limiter
https://github.com/alisaifee/flask-limiter

how to know if noatime or relatime is default mount option in kernel?
https://superuser.com/questions/318293/how-to-know-if-noatime-or-relatime-is-default-mount-option-in-kernel

Why is cat not changing the access time?
https://superuser.com/questions/464290/why-is-cat-not-changing-the-access-time/464737#464737

En savoir plus...:
Flask

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.