angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Blacklisting van IP-adressen op uw Flask website die draait op Linux

Soms wilt u IP adressen direct blokkeren. Dit bericht beschrijft een methode hoe u dit kunt doen.

16 april 2020
In Flask
post main image
https://unsplash.com/@vladbahara

Je hebt een website en het werkt prima. Maar u merkt dat bepaalde bezoekers met uw formulieren proberen te knoeien. Ze komen van specifieke IP-adressen. Dan zijn er ook nog bots die uw site aan het scannen zijn. Sommige zijn nodig, maar andere moeten wegblijven. Haat je dit niet? Ik wel. In het verleden heb ik ooit een module geschreven die heel langzaam, byte voor byte, een niet zo leuke reactie teruggaf, waardoor hun systemen vertraagd werden. Of stuurde een nooit eindigende hoeveelheid gegevens terug. Maar dat is een ander verhaal.

Voorlopig wil ik me richten op een andere methode: het blokkeren van deze verzoeken. Geef gewoon een HTTP 403 Forbidden terug. Ik wil dit on-the-fly kunnen doen vanuit mijn website admin sectie. Daar specificeren we de IP-adressen of de reeks IP-adressen die we willen blokkeren. Er zijn ook andere manieren om dit te doen, zoals het gebruik van .htaccess bestanden en webserver instellingen. Ik zal ze onderaan dit bericht vermelden.

Verschillende redenen om te blokkeren

Ik heb al gezegd dat een van de redenen om de toegang tot uw site te blokkeren is om kwaadwillende bezoekers te blokkeren. Ze willen zien hoe ze uw site kunnen breken, uw commentaarsectie kunnen vullen met reclame of anderszins gekke berichten. Er zijn vele redenen waarom dit wordt gedaan door ik geloof dat een van hen is dat ze willen dwingen uw te grijpen een derde partij anti-spam plugin. Deze kunnen zeer effectief zijn als ze verbinding maken met enorme databases met spam-informatie. Maar als we de privacy van onze bezoekers willen respecteren, kunnen we zo'n plugin niet gebruiken. We moeten andere manieren gebruiken, en een laatste redmiddel is vaak het blokkeren van IP-adressen.

Het kan ook nodig zijn om bepaalde bots te blokkeren die uw site scannen. Sommige bots genereren gekke hoeveelheden verkeer. Ik heb al het verkeer naar deze site voor een bepaalde periode gecontroleerd en het bleek dat slechts 10%, waarschijnlijk nog minder, van de verzoeken afkomstig was van echte bezoekers! Natuurlijk zijn niet alle bots slecht, maar sommige houden zich echt niet aan de regels. De meeste bots zijn te herkennen aan de User Agent string. Ik vond de volgende twee die ik echt wil blokkeren:

  • SemrushBot
  • AhrefsBot

Wees voorzichtig met wat te blokkeren, veel bots worden gebruikt om uw site te krijgen in de resultaten van de zoekmachine. SemrushBot gaat over SEO, ik gebruik dit momenteel niet. Het blokkeren van gebruikersagenten komt in dit bericht niet aan de orde. Het zal niet zo vaak veranderen en je kunt blokken op andere manieren instellen.

Goede dingen van ongewenste verzoeken

Als u een goede logging implementeert dan kunt u ook profiteren van ongewenste verzoeken. In de onderstaande lijst staan enkele verzoeken die een HTTP 404-fout voor deze site hebben veroorzaakt:

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

We zien dat bots zoeken naar het bestand wlwmanifest.xml. Dit blijkt een bestand te zijn dat gekoppeld is aan 'Windows Live Writer', een door Microsoft ontwikkelde blog publishing applicatie die in 2017 is stopgezet en mogelijk kwetsbaar is. Een andere aanval is op zoek naar PHPUnit, een PHP eenheid die framework test. Deze bevatte een kwetsbaarheid die mogelijk nog niet is gepatcht. Andere aanvalsbots kunnen URL's genereren die een HTTP 500-fout veroorzaken. Dit kan bedoeld zijn, maar kan ook worden veroorzaakt door zwakke punten van uw site.

Het goede nieuws is dat u deze informatie kunt gebruiken om uw site te verbeteren. Zorg er altijd voor dat u de juiste logging implementeert, fouten geven u zeer waardevolle informatie!

Beperk tot IPv4 IP adressen alleen

Het blokkeren van bezoekers per IP-adres heeft zijn beperkingen. Veel mensen op het internet krijgen hun IP-adres wanneer ze verbinding maken met een server met behulp van DHCP. Dit geldt vooral voor mobiele telefoons. Wees dus voorzichtig met wat je moet blokkeren.

Dan is er ook nog IPv6 die ontworpen is om de beperkte beschikbaarheid van IPv4 adressen te overwinnen. Hoewel sommige rapporten aangeven dat 30% van het internetverkeer op IPv6 staat, is het aantal servers dat daadwerkelijk IPv6 heeft ingeschakeld veel minder. Dit betekent gelukkig dat er op dit moment geen reden is om uw server te migreren naar IPv6 . Het blokkeren van spam met IPv6 is mogelijk met deze methode maar er is een gotcha.

Admin-bewerkingen en Blacklisted IP-adres-regels

In de admin wil ik de IP-adressen opgeven die ik op de zwarte lijst wil zetten. Er is een tabel met de IP-adressen die op de zwarte lijst staan. Voor IP-adressen wil ik de IP-adressen als volgt kunnen specificeren:

  1. Een enkel IP-adres, voorbeeld: 1.2.3.4
  2. Een IP netwerk, voorbeeld: 1.2.3.0/24
  3. Een IP-adresbereik, voorbeeld: 1.2.3.6-1.2.4.2

Ik specificeer één daarvan in één record en ik noem dit een 'Blacklisted IP Address Rule'.

Caching om toegang tot de database te voorkomen

We willen zeker niet bij elk verzoek toegang tot de database om te zien of het verzoek wordt toegestaan. Dat zou verzoeken vertragen. Daarom gebruiken we caching. In plaats van de database te bevragen controleren we eerst de cache om te zien of het IP-adres al eerder is gebruikt. Voor elk IP adres hebben we een vlag die 'allowed' heet. Als het waar is dan is de toegang toegestaan, als het foutief is dan is de toegang geblokkeerd.

Als het IP-adres in de cache staat, zijn we klaar, gaan we verder of blokkeren we. Als het IP adres niet in de cache staat controleren we of het in de Blacklisted IP Address Rules staat. Het resultaat wordt toegevoegd aan de cache, en de volgende keer dat een aanvraag met dit IP-adres op onze site komt, staan de gegevens in de cache en is de database niet op te vragen.

Toevoegen en verwijderen van Blacklisted IP Adres Regels

Stel dat we honderden, duizenden items in onze cache hebben. Nu willen we wijzigingen aanbrengen via de admin door een Blacklisted IP Address Rule toe te voegen of een Blacklisted IP Address Rule te verwijderen.

Het toevoegen of verwijderen van een regel is niet triviaal, want de regel kan IP-adressen bevatten die al in de cache staan. Wat moet er gedaan worden met onze cachewaarden? De meest eenvoudige manier is om de cache door te spoelen en opnieuw op te laten bouwen. Dit zal de volgende verzoeken voor een korte tijd vertragen. De enige andere manier is om de IP adressen in de cache te scannen en te controleren of ze overeenkomen met de toegevoegde of verwijderde Blacklisted IP Address Rule. Als ze overeenkomen, verwijderen we ze uit de cache. Ik heb een aantal ideeën hoe dit te implementeren, maar heb dit nog niet gedaan.

Tijdstempels toevoegen

Voor maximale prestaties is de IP adressen informatie in de cache alleen-lezen en vervalt deze niet. Dit betekent dat het in de loop van de tijd enorm kan groeien als je veel bezoekers hebt. Omdat de meeste bezoekers slechts enkele minuten toegang hebben tot uw site, kunnen we een tijdstempel toevoegen aan de in de cache opgeslagen IP-adressen, dat bij elke toegang wordt geüpdatet. De timestamp maakt het eenvoudig om oude vermeldingen te verwijderen.

Verzoeken op hetzelfde moment

Stel dat twee verzoeken, verzoek A en verzoek B, tegelijkertijd aankomen, beide met hetzelfde IP adres. Als ze niet in de cache staan, zullen ze allebei controleren of hun IP-adres geblokkeerd is door te zoeken in de Blacklist IP Address Rules tabel. Dan werken ze beiden het cache_access item bij. Request A maakt eerst het toegestane item aan. Maar dan maakt request B het toegestane item aan en overschrijft het toegestane item van request A. Hetzelfde geldt als we de timestamp van het cache-item willen updaten. Dit lijkt misschien slecht, maar is niet echt zo slecht. We moeten er alleen voor zorgen dat het aanmaken atomair is.

Het Linux bestandssysteem gebruiken als cache

Voorlopig kies ik ervoor om de cache met bestanden te implementeren. Het Linux bestandssysteem is snel genoeg om dit af te handelen voor mijn applicatie. Ik wil niet iets als Redis toevoegen, ik wil de afhankelijkheden minimaal houden.

Als we een 'toegestaan' bestand per IP-adres hebben, dan kan het bestand klein zijn, de inhoud is 0 (geblokkeerd) of 1 (toegestaan). Om te voorkomen dat er een groot aantal bestanden in een directory staan en dat het zoeken traag verloopt, maken we subdirectories aan op basis van het IP adres. We splitsen het IP adres op door de punt ('.') en gebruiken dit om directories aan te maken. De tijdstempel van het 'toegestane' bestand verandert automatisch wanneer het bestand wordt gelezen. In Linux hebben we de volgende timestamps:

  • mtime (ls -l)
    De laatste keer dat de inhoud van het bestand werd gewijzigd
  • ctime
    De laatste keer dat de status van het bestand, bijvoorbeeld de rechten, is gewijzigd.
  • atime (ls -lu)
    De laatste keer dat het bestand werd gelezen

Voor ons doel kunnen we atime gebruiken als tijdstempel. We hoeven de tijd van het bestand niet bij te werken. Er is een probleem als u de inhoud van de toegestane bestanden in de admin wilt kunnen tonen. Dit zou de bestanden lezen en de tijdstempels veranderen. We kunnen dit verhelpen door een kopie van het 'toegestane' bestand aan te maken. Het inlezen van de kopie verandert de tijd van het originele 'toegestane' bestand niet.

Een waarschuwing bij het gebruik van de Linux toegangstijdstip

Er is veel informatie op het internet over Linux timestamps, maar slechts weinigen vermelden dat dit misschien niet werkt zoals verwacht. Ik nodig u uit om de onderstaande links hierover te bekijken. U kunt bijvoorbeeld controleren of relatime een mount-optie is met dit commando:

cat /proc/mounts | grep relatime

De samenvatting is:

  • Het bijwerken van elke meting is standaard uitgeschakeld om redenen van prestatievermogen.
  • Sinds kernel 2.6.30 is relatime de standaard optie
  • Sinds kernel 2.6.30 wordt de laatste toegangstijd van een bestand altijd bijgewerkt als het meer dan 1 dag oud is.

Dit betekent dat we nog steeds gebruik kunnen maken van de tijd, maar dat we een resolutie van een dag moeten respecteren. Geen probleem voor mij, maar wacht, laten we testen of dit echt werkt.

ls -l

Het resultaat is:

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

Vervolgens willen we de tijd, of toegangstijd, van het toegestane bestand zien:

stat allowed

Het resultaat is:

  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: -

Nu veranderen we de toegangstijd naar de vorige dag:

sudo touch -a -t 202004151530.02 allowed

Het resultaat van het stat commando laat zien dat de toegangstijd een dag eerder is:

  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: -

Nu genereren we een verzoek op de website en na het verzoek draaien we het stat commando opnieuw:

  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: -

De toegangstijd is bijgewerkt tot vandaag. Latere verzoeken werken de toegangstijd niet meer bij. Ik heb vandaag iets geleerd, zoals verwacht.

Implementatie details

Ik heb de klas CachedAccess genoemd. In Flask's before_request installeer ik het als volgt:

    @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)

En hier zijn de (belangrijke) delen van de klas:

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

Dit is niet echt moeilijk. Ik converteer het IP adres naar een Unsigned Int zodat we kunnen controleren of het zich in een IP netwerk of IP adres bereik bevindt. Als er een onverwachte fout optreedt, log ik de fout in en sta ik het IP-adres toe. Dit betekent dat we onverwachte verzoeken niet blokkeren.

Ontwikkeling en productie

Bij de ontwikkeling zult u waarschijnlijk veel verzoeken geblokkeerd zien tijdens het testen. De reden hiervoor is dat afbeeldingen, Javascript bestanden, etc. ook door de Flask ontwikkelingsserver worden bediend. U kunt deze verzoeken filteren in uw code:

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

Bij de productie ga ik ervan uit dat je al je statische content op de webserver, Nginx, Apache, serveert, wat betekent dat er geen tijd wordt verspild. We blokkeren alleen verzoeken om de code, afbeeldingen etc. worden niet geblokkeerd.

Blokkeren met Nginx

Ik wilde mijn Nginx webserver niet controleren om het eenvoudig te houden. Maar het is niet zo moeilijk om het te vertellen om verzoeken te blokkeren. Als u Nginx gebruikt, kunt u een paar regels toevoegen om meerdere user -agenten te blokkeren, en wel als volgt:

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

En om meerdere IP-adressen te blokkeren die u kunt gebruiken:

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

Maar dit is niet wat we willen. We willen dynamische blokkering. Er zijn verschillende manieren om dit te doen, maar natuurlijk moet je veel meer betrokken raken bij Nginx details. Er zijn genoeg voorbeelden op het internet van hoe dit te doen.

Samenvatting

Ik wilde echt een on-the-fly IP adres blacklisting implementeren en dat bleek niet zo moeilijk. Ik heb op dit moment niet alles geïmplementeerd. Dit betekent geen slimme updates na het toevoegen of verwijderen van Blacklisted IP Address Rules. In plaats daarvan heb ik een knop 'flush cache' die ik kan indrukken nadat ik wijzigingen heb aangebracht in de Blacklist-tabel. Het is als een 'rm -R' in Python.

De Linux toegangstijdstempel vertraagde me bij het schrijven van dit bericht, ik heb de toegangstijd nooit gebruikt maar nu weet ik de eigenaardigheden ervan. Linux werkt de toegangstijd één keer per dag bij, dat vind ik prima.

Ik betwijfel of je betere prestaties kunt krijgen, maar je wilt misschien kijken naar andere opties zoals het cachen van het item in het geheugen. Je zou TTLCache van Python cachetools kunnen gebruiken.

Links / credits

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

Lees meer

Flask

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.