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

Poner en la lista negra las direcciones IP de su sitio web Flask que se ejecuta en Linux

A veces quieres bloquear las direcciones IP inmediatamente. Este post describe un método para hacerlo.

16 abril 2020
En Flask
post main image
https://unsplash.com/@vladbahara

Tienes un sitio web y funciona bien. Pero te das cuenta de que ciertos visitantes están tratando de meterse con tus formularios. Vienen de direcciones IP específicas. También hay bots que están escaneando tu sitio. Algunos son necesarios pero otros deben mantenerse alejados. ¿No odias esto? Sí, lo odio. En el pasado escribí una vez un módulo que devolvía una respuesta no tan agradable muy lentamente, byte a byte, ralentizando sus sistemas. O que devolvía una cantidad interminable de datos. Pero esa es otra historia.

Por ahora quiero centrarme en otro método: bloquear estas peticiones. Simplemente devuelve un HTTP 403 Forbidden. Quiero ser capaz de hacer esto sobre la marcha desde la sección de administración de mi sitio web. Allí especificamos las direcciones IP o el rango de direcciones IP que queremos bloquear. También hay otras formas de hacerlo, como usar los archivos .htaccess y la configuración del servidor web. Los mencionaré al final de este artículo.

Varias razones para bloquear

Ya mencioné que una de las razones para bloquear el acceso a su sitio es bloquear a los visitantes maliciosos. Quieren ver cómo pueden romper tu sitio, llenar tu sección de comentarios con publicidad o mensajes locos. Hay muchas razones por las que creo que una de ellas es que quieren forzarte a tomar un plugin anti-spam de terceros. Estos pueden ser muy efectivos ya que se conectan a enormes bases de datos con información de spam. Pero si queremos respetar la privacidad de nuestros visitantes no podemos usar tal plugin. Debemos usar otras formas, y un último recurso a menudo es el bloqueo de direcciones IP.

También puede ser necesario bloquear ciertos bots que escanean su sitio. Algunos bots generan cantidades locas de tráfico. Revisé todo el tráfico de este sitio por un cierto período y pareció que sólo el 10%, probablemente menos, de las solicitudes eran de visitantes reales! Por supuesto que no todos los bots son malos, pero algunos realmente no respetan las reglas. La mayoría de los bots pueden ser identificados por la cadena de Agente de Usuario. Encontré los siguientes dos que realmente quiero bloquear:

  • SemrushBot
  • AhrefsBot

Tenga cuidado con lo que bloquea, muchos bots se utilizan para que su sitio aparezca en los resultados de los motores de búsqueda. SemrushBot es sobre SEO, no estoy usando esto en este momento. El bloqueo de agentes de usuario no está cubierto en este post. No cambiará tan a menudo y puedes establecer bloqueos de otras maneras.

Lo bueno de las peticiones no deseadas

Si se implementa un registro adecuado, también se puede aprovechar de las solicitudes no deseadas. La siguiente lista muestra algunas peticiones que causaron un error HTTP 404 para este sitio:

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

Vemos que los bots están buscando el archivo wlwmanifest.xml. Este parece ser un archivo asociado con 'Windows Live Writer', una aplicación de publicación de blogs desarrollada por Microsoft que fue descontinuada en 2017 y puede ser vulnerable. Otro ataque está buscando PHPUnit, una unidad PHP probando framework. Esto contenía una vulnerabilidad que puede no haber sido parcheada todavía. Otros bots de ataque pueden generar URLs que causan un error HTTP 500. Esto puede ser intencionado pero también puede ser causado por debilidades de su sitio.

La buena noticia es que puedes usar esta información para mejorar tu sitio. Siempre asegúrese de implementar un registro adecuado, los errores le dan información muy valiosa!

Limitado a IPv4 sólo direcciones IP

Bloquear a los visitantes por la dirección IP tiene sus limitaciones. Mucha gente en Internet obtiene su dirección IP cuando se conecta a un servidor usando DHCP. Esto es mayormente cierto para los teléfonos móviles. Así que ten cuidado con lo que bloqueas.

También está IPv6 que fue diseñado para superar la limitada disponibilidad de las direcciones IPv4 . Aunque algunos informes afirman que el 30% del tráfico de Internet está en IPv6, el número de servidores que realmente han habilitado IPv6 es mucho menor. Esto afortunadamente significa que no hay razón para migrar su servidor a IPv6 en este momento. Bloquear el spam con IPv6 es posible con este método pero hay un "gotcha".

Operaciones administrativas y reglas de direcciones IP de la lista negra

En el administrador quiero especificar las direcciones IP que quiero poner en la lista negra. Hay una tabla con registros de direcciones IP en la lista negra. Para las direcciones IP quiero poder especificar las direcciones IP de la siguiente manera:

  1. Una sola dirección IP, ejemplo: 1.2.3.4
  2. Una red IP, ejemplo: 1.2.3.0/24
  3. Un rango de direcciones IP, por ejemplo: 1.2.3.6-1.2.4.2

Especifico uno de estos en un solo registro y lo llamo "Regla de la lista negra de direcciones IP".

Caching para evitar el acceso a la base de datos

Ciertamente no queremos acceder a la base de datos en cada solicitud para ver si la solicitud está permitida. Eso ralentizaría las solicitudes. Por eso usamos el caching. En lugar de consultar la base de datos, primero revisamos el caché para ver si la dirección IP accedió al sitio antes. Para cada dirección IP tenemos un indicador llamado "permitido". Si es Verdadero entonces el acceso está permitido, si es Falso entonces el acceso está bloqueado.

Si la dirección IP está en la caché, hemos terminado, continuamos o bloqueamos. Si la dirección IP no está en el caché, comprobamos si está en la lista negra de reglas de direcciones IP. El resultado se añade al caché, y la próxima vez que una solicitud con esta dirección IP llegue a nuestro sitio, los datos estarán en el caché y la base de datos no será consultada.

Añadir y eliminar las reglas de la lista negra de direcciones IP

Supongamos que tenemos cientos, miles de elementos en nuestro caché. Ahora queremos hacer cambios usando el administrador, ya sea agregando una Regla de Dirección IP de la Lista Negra o removiendo una Regla de Dirección IP de la Lista Negra.

Añadir o eliminar una regla no es trivial porque la regla puede incluir direcciones IP que ya están en la caché. La forma más simple es vaciar el caché y dejar que se reconstruya de nuevo. Esto ralentizará las siguientes peticiones durante un corto tiempo. La única otra forma es escanear las direcciones IP en la caché y comprobar si coinciden con la regla de la lista negra de direcciones IP añadida o eliminada. Si coinciden, las eliminamos de la caché. Tengo algunas ideas de cómo implementar esto, pero aún no lo he hecho.

Añadiendo marcas de tiempo

Para un máximo rendimiento, la información de las direcciones IP en la caché es de sólo lectura y no caduca. Esto significa que puede crecer enormemente con el tiempo si tienes muchos visitantes. Debido a que la mayoría de los visitantes acceden a su sitio por unos pocos minutos, podemos agregar una marca de tiempo a las direcciones IP en caché que se actualiza en cada acceso. La marca de tiempo hace que sea fácil eliminar las entradas antiguas.

Las solicitudes en el mismo momento

Supongamos que dos peticiones, la petición A y la petición B, llegan al mismo tiempo, ambas usando la misma dirección IP. Si no están en la caché ambas comprobarán si su dirección IP está bloqueada buscando en la tabla de Reglas de la Lista Negra de Direcciones IP. Luego ambas actualizan el ítem cached_access. La solicitud A crea primero el elemento permitido. Pero luego la solicitud B crea el ítem permitido, sobrescribiendo el ítem permitido de la solicitud A. Lo mismo ocurre si queremos actualizar la marca de tiempo del ítem en caché. Esto puede parecer malo, pero en realidad no lo es tanto. Sólo debemos asegurarnos de que la operación de creación es atómica.

Usando el sistema de archivos Linux como caché

Por el momento elijo implementar el cacheo con los archivos. El sistema de archivos Linux es lo suficientemente rápido para manejar esto para mi aplicación. No quiero añadir algo como Redis, quiero mantener las dependencias al mínimo.

Si tenemos un archivo "permitido" por dirección IP, entonces el archivo puede ser pequeño, el contenido es 0 (bloqueado) o 1 (permitido). Para evitar un gran número de archivos en un directorio y una búsqueda lenta, creamos subdirectorios basados en la dirección IP. Dividimos la dirección IP por el punto ('.') y la usamos para crear directorios. La marca de tiempo del archivo "permitido" cambia automáticamente cuando se lee el archivo. En Linux tenemos las siguientes marcas de tiempo:

  • mtime (ls -l)
    La última vez que el contenido del archivo fue modificado
  • ctime
    La última vez que el estado del archivo, por ejemplo, los permisos, cambió
  • atime (ls -lu)
    La última vez que el archivo fue leído

Para nuestro propósito podemos usar un tiempo como una marca de tiempo. No tenemos que actualizar la hora del archivo. Hay un problema si quieres poder mostrar el contenido de los archivos permitidos en el admin. Esto leería los archivos y cambiaría las marcas de tiempo. Podemos solucionarlo creando una copia del archivo "permitido". La lectura de la copia no cambia la hora del archivo original "permitido".

Una advertencia cuando se utiliza el Linux tiempo de acceso atime

Hay mucha información en Internet sobre las marcas de tiempo de Linux pero sólo muy pocos mencionan que esto puede no funcionar como se espera. Les invito a mirar los enlaces de abajo sobre esto. Por ejemplo, puedes comprobar si relatime es una opción de montaje con este comando:

cat /proc/mounts | grep relatime

El resumen es:

  • La actualización a tiempo en cada lectura está desactivada por defecto por razones de rendimiento
  • Desde el kernel 2.6.30, el relatime es la opción por defecto
  • Desde el kernel 2.6.30, la última hora de acceso a un archivo siempre se actualiza si tiene más de 1 día de antigüedad

Esto significa que todavía podemos usar un tiempo pero debemos respetar una resolución de un día. No hay problema para mí pero espera, vamos a probar si esto realmente funciona.

ls -l

El resultado es:

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

A continuación queremos ver el atime, o tiempo de acceso, del archivo permitido:

stat allowed

El resultado es:

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

Ahora cambiamos la hora de acceso al día anterior:

sudo touch -a -t 202004151530.02 allowed

El resultado del comando stat muestra que el tiempo de acceso es un día antes:

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

Ahora generamos una petición en el sitio web y después de la petición ejecutamos el comando stat de nuevo:

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

El tiempo de acceso se actualizó hasta hoy. Las solicitudes posteriores ya no actualizan el tiempo de acceso. Trabajando como se esperaba, he aprendido algo hoy.

Detalles de la implementación

Llamé a la clase CachedAccess. En Flask's before_request lo instancio de la siguiente manera:

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

Y aquí están las partes (importantes) de la clase:

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

Esto no es realmente muy difícil. Convierto la dirección IP en una Int sin firmar para poder comprobar si está en una red IP o en un rango de direcciones IP. Si se produce un error inesperado, registro el error y permito la dirección IP. Esto significa que no bloqueamos las solicitudes inesperadas.

Desarrollo y producción

En el desarrollo probablemente verá muchas solicitudes bloqueadas mientras se realizan las pruebas. La razón es que las imágenes, los archivos Javascript , etc. también son servidos por el servidor de desarrollo Flask . Puedes filtrar estas peticiones en tu código:

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

En la producción asumo que está sirviendo todo su contenido estático por el servidor web, Nginx, Apache, lo que significa que no se pierde tiempo. Sólo bloqueamos las solicitudes al código, las imágenes, etc. no se bloquean.

Bloqueo con Nginx

No quiero controlar mi servidor web Nginx para mantenerlo simple. Pero no es tan difícil decirle que bloquee las solicitudes. Si usas Nginx, puedes agregar unas pocas líneas para bloquear múltiples agentes user de la siguiente manera:

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

Y para bloquear múltiples direcciones IP que puedes usar:

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

Pero esto no es lo que queremos. Queremos un bloqueo dinámico. Hay varias maneras de hacerlo, pero por supuesto tendrás que involucrarte mucho más en las especificaciones de Nginx . Hay suficientes ejemplos en Internet de cómo hacer esto.

Resumen

Realmente quería implementar una lista negra de direcciones IP sobre la marcha y no parecía tan difícil. No implementé todo en este momento. Esto significa que no hay actualizaciones inteligentes después de añadir o eliminar las reglas de la lista negra de direcciones IP. En su lugar, tengo un botón 'flush cache' que puedo pulsar después de hacer cambios en la tabla de la Lista Negra. Es como un 'rm -R' en Python.

La marca de tiempo de acceso Linux me retrasó al escribir este post, nunca usé el tiempo de acceso pero ahora conozco sus peculiaridades. Linux actualiza la hora de acceso una vez al día, eso está bien para mí.

Dudo que puedas obtener un mejor rendimiento pero tal vez quieras mirar otras opciones como el almacenamiento en caché del elemento en la memoria. Podrías usar TTLCache de las herramientas de caché de Python .

Enlaces / créditos

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

Leer más

Flask

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.